blob: 832af13a2ff08f09ff26a317027874ed80897dae [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_Tiler_DEFINED
#define skgpu_graphite_sparse_strips_Tiler_DEFINED
#include "include/private/SkAssert.h"
#include "include/private/SkTDArray.h"
#include "src/gpu/graphite/sparse_strips/Polyline.h"
#include "src/gpu/graphite/sparse_strips/SparseStripsTypes.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <string>
#include <tuple>
namespace skgpu::graphite {
// A sparse strips tile, contains:
// 1) The top left corner of the tile in device space, in tile sized units.
// 2) The coarse winding (W) of the tile
// 3) The intersection mask (R | L | T | B) of the tile
// 4) The index of their parent line in the array backing polyline
//
// TODO (thomsmit): Should move onto SparseStripsTypes?
struct Tile : public IntersectionBits {
uint16_t x;
uint16_t y;
/// Contains the parent line's index and the intersection/winding mask.
/// MSB LSB
/// 31------------------------------------------------------5|4|3|2|1|0|
/// | Parent Line Index (27 bits) |W|R|L|B|T|
uint32_t fPackedLineIdxIntersectionMask;
Tile() : x(0), y(0), fPackedLineIdxIntersectionMask(0) {}
Tile(uint16_t x, uint16_t y, uint32_t lineIdx, uint32_t intersectionMask)
: x(x)
, y(y)
, fPackedLineIdxIntersectionMask((lineIdx << IntersectionBits::INT_MASK_SHIFT) |
intersectionMask) {
SkASSERT(intersectionMask < (1 << IntersectionBits::INT_MASK_SHIFT));
}
// When sorting we place y in the highest bits followed by x, so that tiles are grouped by row,
// left to right. Note, lineIdx is placed in the lower bits of the sorting key so that tiles
// from the same parent line at the same geometric location are adjacent and have better cache
// locality when processed in makeStrips.
//
// TODO (thomsmit): try holding as u64 and converting for member access instead of converting
// for sorting.
SK_ALWAYS_INLINE uint64_t toBits() const {
return (static_cast<uint64_t>(y) << 48) | (static_cast<uint64_t>(x) << 32) |
static_cast<uint64_t>(fPackedLineIdxIntersectionMask);
}
bool operator<(const Tile& other) const { return toBits() < other.toBits(); }
bool operator==(const Tile& other) const { return toBits() == other.toBits(); }
uint32_t lineIdx() const {
return fPackedLineIdxIntersectionMask >> IntersectionBits::INT_MASK_SHIFT;
}
uint32_t intersectionMask() const {
return fPackedLineIdxIntersectionMask & IntersectionBits::INTERSECTION_MASK;
}
bool coarseWinding() const {
return (fPackedLineIdxIntersectionMask & IntersectionBits::W) != 0;
}
bool hasLeftIntersection() const {
return (fPackedLineIdxIntersectionMask & IntersectionBits::L) != 0;
}
// Responsible for consuming the tile coordinates and intersection mask produced by the Tiler
// and producing the exact points at which the parent line intersects the edges of the tile.
// It also safely handles when there is only one edge intersection, either from the line ending
// inside the tile body, or a horizontal edge touch.
//
// This function provides the following guarantees:
// 1) Y-Sorting: The resulting points are always sorted top-to-bottom.
// 2) Interior Endpoints: If a line ends inside the tile, its exact endpoint is preserved and
// returned.
// 3) Zero-Width Degenerates: A single-point intersection (e.g., a horizontal graze or corner
// touch) produces a second point along that exact same edge, yielding safe, zero-width
// geometry.
// 4) Boundary Snapping: Edge intersections are explicitly snapped to the exact bounding
// coordinate of the tile.
//
// (Note: Guarantees 2 and 4 ensure downstream logic can safely rely on floating-point equality
// checks).
//
// Note, this function is tightly coupled to Tiler and MakeStrips. If the incoming tile
// coordinates are incorrect, the results from this function are meaningless. It assumes that:
// 1) Derivatives resulting from division by zero (e.g. perfectly vertical or horizontal lines)
// will be set to zero.
// 2) Only two bits are ever set in the mask. (Expects that perfect corner touches are tie
// broken by the Tiler.)
template <uint16_t kWidth, uint16_t kHeight>
static SK_ALWAYS_INLINE std::tuple<Line, bool, bool> ClipToTile(
const Line& line,
const std::array<SkPoint, 2>& tileBounds,
const std::array<float, 4>& derivatives,
uint32_t intersectionMask,
bool canonicalXDir,
bool canonicalYDir) {
constexpr float width = static_cast<float>(kWidth);
constexpr float height = static_cast<float>(kHeight);
const SkPoint topLeft = tileBounds[0];
const float tileMinX = tileBounds[0].fX;
const float tileMinY = tileBounds[0].fY;
const float tileMaxX = tileBounds[1].fX;
const float tileMaxY = tileBounds[1].fY;
const float dx = derivatives[0];
const float dy = derivatives[1];
// A line's direction dictates which edges it can cross from the outside-in (entry) versus
// inside-out (exit). For example, if dx > 0 (canonicalXDir = true), the vector is monotonic
// in the positive X direction. It is impossible to intersect the right edge as an entry
// point, or the left edge as an exit point. This reduces the number of edges which must be
// checked from four to two candidate entry and two candidate exit edges.
uint32_t maskVIn, maskVOut;
float boundVIn, boundVOut;
if (canonicalXDir) {
maskVIn = L;
boundVIn = tileMinX;
maskVOut = R;
boundVOut = tileMaxX;
} else {
maskVIn = R;
boundVIn = tileMaxX;
maskVOut = L;
boundVOut = tileMinX;
}
uint32_t maskHIn, maskHOut;
float boundHIn, boundHOut;
if (canonicalYDir) {
maskHIn = T;
boundHIn = tileMinY;
maskHOut = B;
boundHOut = tileMaxY;
} else {
maskHIn = B;
boundHIn = tileMaxY;
maskHOut = T;
boundHOut = tileMinY;
}
const float invDx = derivatives[2];
const float invDy = derivatives[3];
// Check the candidate edges against the intersection mask
const uint32_t entryHits = intersectionMask & (maskVIn | maskHIn);
const uint32_t exitHits = intersectionMask & (maskVOut | maskHOut);
auto clipPt = [&](SkPoint p, uint32_t hits, uint32_t maskH, float boundH, float boundV)
-> std::pair<SkPoint, bool> {
bool isLeft = false;
if (hits != 0) {
const bool useH = (intersectionMask & maskH) != 0;
const float bound = useH ? boundH : boundV;
const float start = useH ? line.p0.fY : line.p0.fX;
const float invD = useH ? invDy : invDx;
const float t = (bound - start) * invD;
p.fX = line.p0.fX + t * dx;
p.fY = line.p0.fY + t * dy;
if (useH) {
p.fY = bound;
p.fX -= topLeft.fX;
p.fY -= topLeft.fY;
p.fX = std::clamp(p.fX, 0.0f, width);
} else {
p.fX = bound;
p.fX -= topLeft.fX;
p.fY -= topLeft.fY;
p.fY = std::clamp(p.fY, 0.0f, height);
isLeft = (bound == tileMinX);
}
} else {
p.fX -= topLeft.fX;
p.fY -= topLeft.fY;
p.fX = std::clamp(p.fX, 0.0f, width);
p.fY = std::clamp(p.fY, 0.0f, height);
}
return {p, isLeft};
};
auto [pEntry, entryLeft] = clipPt(line.p0, entryHits, maskHIn, boundHIn, boundVIn);
auto [pExit, exitLeft] = clipPt(line.p1, exitHits, maskHOut, boundHOut, boundVOut);
// TODO (thomsmit): add a section to the function preamble describing the what these bools
// guarantee. Explain, how this works. (Non-Obvious why entry/exit maps to top/bottom.)
bool topIsOnLeftEdge = canonicalYDir ? entryLeft : exitLeft;
bool botIsOnLeftEdge = canonicalYDir ? exitLeft : entryLeft;
// Guarantee predictable winding order for downstream rasterization stages.
if (canonicalYDir) {
return {{pEntry, pExit}, topIsOnLeftEdge, botIsOnLeftEdge};
} else {
return {{pExit, pEntry}, topIsOnLeftEdge, botIsOnLeftEdge};
}
}
};
// Make sure the struct is a size convenient to move around, as tiles will be sorted.
static_assert(sizeof(Tile) == 8);
// Container that holds the tiles and manages their state (sorting). Templated to expose the tile
// size as statically tunable parameter which can be tested within the same compilation unit
template <uint16_t kTileWidth, uint16_t kTileHeight>
class Tiles : public IntersectionBits {
public:
Tiles() {}
void reset() {
fTileBuf.clear();
}
void sortTiles(int32_t offset) {
std::sort(fTileBuf.begin() + offset, fTileBuf.end());
}
void sortTiles() {
sortTiles(0);
}
const SkTDArray<Tile>& getTiles() const { return fTileBuf; }
// This function has two purposes:
// 1) The viewport is divided into tiles, whose dimensions are given by the template
// parameters. Tiles act as a "super coarse rasterization stage," which elides carrying a
// scanline for each MSAA subsample point. (Without this technique, we would require 64
// scanlines for an 8 tall tile with 8xMSAA). So for each line produced by
// flattening---contained inside Polyline---we need to find which tiles this line
// intersects. Tile edge touches are vertical exclusive, horizontal inclusive. This is
// because:
// A) In the vertical case, inclusivity would cause the coarse winding to be double
// counted or negated by a single-point tile produced by a grazing touch.
// B) In the horizontal case, the inclusivity is necessary because the tile produced by
// the succeeding line may not consider itself left-touching (depending on its
// direction), so the single-point tile is necessary to carry over the left-edge
// winding.
// Using the line's direction enforces mutually exclusive ownership of boundary
// intersections between consecutive segments *of the same direction*, ensuring that the
// left-edge winding is neither lost nor double-counted:
//
// Left edge behavior:
// +----------------------+----------+------------+
// | X Direction | Endpoint | L Bit Set? |
// +----------------------+----------+------------+
// | Left-to-Right (L->R) | Start | No |
// | Left-to-Right (L->R) | End | Yes |
// | Right-to-Left (R->L) | Start | Yes |
// | Right-to-Left (R->L) | End | No |
// +----------------------+----------+------------+
//
// 2) To enable parallel rasterization, we need to establish a source of truth for the line
// intersection points on tiles, such that adjacent tiles agree where intersections occur.
// While calculating exact intersection coordinates here is feasible, it is (relatively)
// computationally expensive, so instead, we defer the heavy math to the GPU/rasterizer and
// produce a lightweight intersection bitmask. This mask unambiguously defines which edges
// of a tile a line segment touches:
//
// Bit representation:
// Bit: 4 | 3 | 2 | 1 | 0
// Val: W | R | L | B | T
//
// - W (Winding): Tracks whether the line touched the top edge of the tile.
// - R/L/B/T: Right, Left, Bottom, and Top edge intersections.
void makeTilesMSAA(const Polyline& polyline, uint16_t viewportWidth, uint16_t viewportHeight) {
SkASSERT(polyline.count() <= MAX_LINES_PER_PATH);
if (viewportWidth == 0 || viewportHeight == 0) {
return;
}
// Will never underflow
uint16_t tileColumns = DivCeil(viewportWidth, kTileWidth) - 1;
uint16_t tileRows = DivCeil(viewportHeight, kTileHeight);
constexpr float invW = 1.0f / static_cast<float>(kTileWidth);
constexpr float invH = 1.0f / static_cast<float>(kTileHeight);
for (auto it = polyline.begin(); it != polyline.end(); ++it) {
auto [line, lineIdx] = *it;
// map line into tile units
float p0X = line.p0.fX * invW;
float p0Y = line.p0.fY * invH;
float p1X = line.p1.fX * invW;
float p1Y = line.p1.fY * invH;
float lineLeftX, lineRightX;
if (p0X < p1X) {
lineLeftX = p0X;
lineRightX = p1X;
} else {
lineLeftX = p1X;
lineRightX = p0X;
}
// If the leftmost point of this line is right of the viewport, cull it. Although we
// cull path verbs right of the viewport in the flattening stage, a right edge crossing
// path verb may still be flattened into lines, some of which may be completely outside
// of the viewport.
if (lineLeftX >= static_cast<float>(tileColumns + 1)) {
// Note, lineLeftX > tileColumns is NOT equiavalent here, and so the + 1 cannot be
// dropped
continue;
}
float lineTopY, lineTopX, lineBottomY, lineBottomX;
if (p0Y < p1Y) {
lineTopY = p0Y;
lineTopX = p0X;
lineBottomY = p1Y;
lineBottomX = p1X;
} else {
lineTopY = p1Y;
lineTopX = p1X;
lineBottomY = p0Y;
lineBottomX = p0X;
}
uint16_t yTopTiles = std::min(f32ToU16Sat(lineTopY), tileRows);
float lineBottomYCeil = std::ceil(lineBottomY);
uint16_t yBottomTiles = std::min(f32ToU16Sat(lineBottomYCeil), tileRows);
// If yTopTiles == yBottomTiles, then the line is either completely above or below the
// viewport OR it is perfectly horizontal and aligned to the tile grid, contributing no
// winding. In either case, it should be culled.
if (yTopTiles >= yBottomTiles) {
// Technically, the `>` part of the `>=` is unnecessary due to clamping, but this
// gives stronger signal.
continue;
}
int32_t p0TileX = static_cast<int32_t>(std::floor(lineTopX));
int32_t p0TileY = static_cast<int32_t>(std::floor(lineTopY));
int32_t p1TileX = static_cast<int32_t>(std::floor(lineBottomX));
int32_t p1TileY;
if (lineBottomY == lineBottomYCeil) {
p1TileY = static_cast<int32_t>(lineBottomY) - 1;
} else {
p1TileY = static_cast<int32_t>(std::floor(lineBottomY));
}
// Each line processed falls into 1 of three categories:
// 1) The line produces a single tile.
// 2) The line is perfectly vertical.
// 3) The line is neither 1 or 2 (general case)
bool notSameTile = (p0TileY != p1TileY) || (p0TileX != p1TileX);
if (notSameTile) {
if (lineLeftX == lineRightX) { // Vertical line case
uint16_t x = std::min(f32ToU16Sat(lineLeftX), tileColumns);
// Process the Top Row (if visible on screen)
uint16_t yStart = yTopTiles;
bool isStartCulled = (lineTopY < 0.0f);
if (!isStartCulled) {
uint32_t winding = (static_cast<float>(yTopTiles) >= lineTopY) ? W : 0;
uint32_t intersectionMask = B | winding;
fTileBuf.push_back(Tile(x, yStart, lineIdx, intersectionMask));
yStart++;
}
// Process all "fully crossed" tiles (W | T | B).
int32_t yEndIdx = std::min(p1TileY, static_cast<int32_t>(tileRows));
for (int32_t yIdx = yStart; yIdx < yEndIdx; ++yIdx) {
uint32_t intersectionMask = W | T | B;
fTileBuf.push_back(
Tile(x, static_cast<uint16_t>(yIdx), lineIdx, intersectionMask));
}
// Process the terminal tile (W | T), if it exists. We only emit this if the
// line actually terminates on the screen, and if we haven't already
// processed/culled it via `yStart`.
if (p1TileY >= yStart && p1TileY < static_cast<int32_t>(tileRows)) {
uint32_t intersectionMask = W | T;
fTileBuf.push_back(
Tile(x, static_cast<uint16_t>(p1TileY), lineIdx, intersectionMask));
}
} else { // General case
float dx = p1X - p0X;
float dy = p1Y - p0Y;
float xSlope = dx / dy;
// Package for the helper functions, with inlining this should be zero cost.
// Changing to a class member regresses perf by ~10% on TilerSortBench
LineContext ctx {
static_cast<uint32_t>(lineIdx),
lineTopX, lineTopY,
lineBottomX, lineBottomY,
xSlope,
lineLeftX, lineRightX,
p0TileX, p0TileY,
p1TileX, p1TileY,
tileColumns
};
if (lineBottomX > lineTopX) { // cannot be equal at this point
runLoops</*kXDir=*/true>(ctx, lineTopY, lineBottomY, yTopTiles, tileRows);
} else {
runLoops</*kXDir=*/false>(ctx, lineTopY, lineBottomY, yTopTiles, tileRows);
}
}
} else { // Single tile case
uint16_t xClamped = std::min(f32ToU16Sat(lineLeftX), tileColumns);
uint32_t winding = static_cast<float>(yTopTiles) >= lineTopY ? W : 0;
fTileBuf.push_back(Tile(xClamped, yTopTiles, lineIdx, winding));
}
}
}
private:
// For now, only support square tiles.
static_assert(kTileWidth == kTileHeight);
struct LineContext {
uint32_t lineIdx;
float topX, topY;
float bottomX, bottomY;
float xSlope;
float lineLeftX, lineRightX;
int32_t p0TileX, p0TileY;
int32_t p1TileX, p1TileY;
uint16_t tileColumns;
};
SkTDArray<Tile> fTileBuf;
static SK_ALWAYS_INLINE uint16_t DivCeil(uint16_t a, uint16_t b) {
return (a + b - 1) / b;
}
static SK_ALWAYS_INLINE uint16_t f32ToU16Sat(float v) {
// std::clamp will catch +/- inf here, but not NaN. However we should never get NaN here.
SkASSERT(!std::isnan(v));
return static_cast<uint16_t>(std::clamp(v, 0.0f, 65535.0f));
}
template<bool kXDir>
SK_ALWAYS_INLINE void pushEdge(const LineContext& ctx,
uint16_t xIdx,
uint16_t y,
float rowTopX,
float rowBottomX,
int32_t canonicalStart,
uint16_t canonicalEnd,
uint32_t windingInput,
bool checkStart,
bool checkEnd) {
// Determine whether this tile is the true start or end of the line within this horizontal
// row. We need to account for clamping because the line may start or end off-screen (e.g.,
// X = -5), but we clamp X to the viewport.
uint32_t uncRowStart = static_cast<uint32_t>(static_cast<int32_t>(xIdx) == canonicalStart);
uint32_t uncRowEnd = static_cast<uint32_t>(xIdx == canonicalEnd);
// Relativize the start/end based on line direction
uint32_t canonicalRowStart = kXDir ? uncRowStart : uncRowEnd;
uint32_t canonicalRowEnd = kXDir ? uncRowEnd : uncRowStart;
// Mask out the Top/Bottom bits if this tile contains the line endpoints.
uint32_t notStartTile = 1;
if (checkStart) {
notStartTile ^= static_cast<uint32_t>((static_cast<int32_t>(xIdx) == ctx.p0TileX) &&
(static_cast<int32_t>(y) == ctx.p0TileY));
}
uint32_t notEndTile = 1;
if (checkEnd) {
notEndTile ^= static_cast<uint32_t>((static_cast<int32_t>(xIdx) == ctx.p1TileX) &&
(static_cast<int32_t>(y) == ctx.p1TileY));
}
uint32_t mask = windingInput;
// If this tile is the start of the row, the line must have entered through the Top edge.
// (Unless it's the line start).
mask |= canonicalRowStart & notStartTile;
// If this tile is the end of the row, the line must have exited through the Bottom edge.
// (Unless it's the line end).
mask |= (canonicalRowEnd & notEndTile) << BOT_SHIFT;
// If a tile is NOT the start of the row, it must have been entered horizontally. If it is
// NOT the end, it must have exited horizontally. Base L/R on the direction of the line.
if constexpr (kXDir) {
mask |= (1 ^ canonicalRowStart) << LEFT_SHIFT;
mask |= (1 ^ canonicalRowEnd) << RIGHT_SHIFT;
} else {
mask |= (1 ^ canonicalRowStart) << RIGHT_SHIFT;
mask |= (1 ^ canonicalRowEnd) << LEFT_SHIFT;
}
// Corner handling
float xLeftF = static_cast<float>(xIdx);
float xRightF = static_cast<float>(xIdx + 1);
uint32_t trc = static_cast<uint32_t>(rowTopX == xRightF) & notStartTile;
uint32_t tlc = static_cast<uint32_t>(rowTopX == xLeftF) & notStartTile;
uint32_t brc = static_cast<uint32_t>(rowBottomX == xRightF) & notEndTile;
uint32_t blc = static_cast<uint32_t>(rowBottomX == xLeftF) & notEndTile;
// If the line hits the exact Top-Left corner, but it is NOT the canonical start of the row,
// we must treat it as a Left intersection to properly bridge the mask to the adjacent tile.
uint32_t tieBreak = tlc & (canonicalRowStart ^ 1);
// Force corners into into purely horizontal intersections. This makes the downstream
// intersection calculation logic simpler.
mask |= (tieBreak | blc) << LEFT_SHIFT;
mask |= (trc | brc) << RIGHT_SHIFT;
mask &= ~(tieBreak | trc);
mask &= ~((blc | brc) << BOT_SHIFT);
fTileBuf.push_back(Tile(xIdx, y, ctx.lineIdx, mask));
}
template<bool kXDir>
SK_ALWAYS_INLINE void processRow(const LineContext& ctx,
uint16_t yIdx,
float rowTopX,
float rowBottomX,
uint32_t wMask,
bool checkStart,
bool checkEnd) {
float lx = std::fmin(rowTopX, rowBottomX);
float rx = std::fmax(rowTopX, rowBottomX);
// Convert floating-point boundaries into discrete integer tile indices. Note:
// `canonicalXStart` preserves the true start (even if negative) before clamping, which is
// required by `pushEdge` to tie-break in some cases.
int32_t canonicalXStart = static_cast<int32_t>(std::floor(lx));
uint16_t canonicalXEnd = f32ToU16Sat(rx);
uint16_t xStart = f32ToU16Sat(lx);
// Clamp the end of the row to the right viewport if necessary.
uint16_t xEndVal = std::min(canonicalXEnd, ctx.tileColumns);
if (xStart <= xEndVal) {
// Process the Leftmost Tile of the row. If this is the *only* tile in the row,
// `isSingle`, it is both the row start and row end so the Winding (W) bit is passed
// regardless of direction.
bool isSingle = xStart == xEndVal;
uint32_t wLeft = (kXDir || isSingle ? W : 0) & wMask;
pushEdge<kXDir>(ctx, xStart, yIdx, rowTopX, rowBottomX, canonicalXStart, canonicalXEnd,
wLeft, checkStart, checkEnd);
// Process all captive "Middle" tiles in this row. These tiles never have vertical
// crossings, and for the purpose of the intersection mask, are identical as they always
// recieve [R | L]. Bulk append with `inner count.`
int innerCount = static_cast<int>(xEndVal) - static_cast<int>(xStart) - 1;
if (innerCount > 0) {
uint32_t innerMask = R | L;
Tile* outTiles = fTileBuf.append(innerCount);
for (int i = 0; i < innerCount; ++i) {
outTiles[i] = Tile(xStart + 1 + i, yIdx, ctx.lineIdx, innerMask);
}
}
// Process the Rightmost Tile of the row. Emitted only if the row spans more than one
// tile (i.e., we haven't already processed this exact tile as the Leftmost Tile).
if (xStart < xEndVal) {
uint32_t wRight = (kXDir ? 0 : W) & wMask;
pushEdge<kXDir>(ctx, xEndVal, yIdx, rowTopX, rowBottomX, canonicalXStart,
canonicalXEnd, wRight, checkStart, checkEnd);
}
}
}
template<bool kXDir>
SK_ALWAYS_INLINE void runLoops(const LineContext& ctx,
float lineTopY,
float lineBottomY,
uint16_t yTopTiles,
uint16_t tileRows) {
// Process the Top Row (if visible on screen)
uint16_t yStart = yTopTiles;
bool isStartCulled = (lineTopY < 0.0f);
if (!isStartCulled) {
float y = static_cast<float>(yStart);
float rowBottomY = std::min(y + 1.0f, lineBottomY);
// Catch perfectly horizontal lines and/or prevent floating point drift
float rowBottomX = (rowBottomY == ctx.bottomY)
? ctx.bottomX
: ctx.topX + (rowBottomY - ctx.topY) * ctx.xSlope;
uint32_t mask = y >= lineTopY ? W : 0;
// The top row might ALSO be the bottom row, so checkEnd = true
processRow<kXDir>(ctx, yStart, ctx.topX, rowBottomX, mask,
/*checkStart=*/true, /*checkEnd=*/true);
yStart++;
}
// Process all "Middle" fully crossed rows; the tiles cannot be the start or the end
int32_t yEndIdx = std::min(ctx.p1TileY, static_cast<int32_t>(tileRows));
for (int32_t yIdx = yStart; yIdx < yEndIdx; ++yIdx) {
float y = static_cast<float>(yIdx);
// Although this seems like duplicate calculation, finding the intersections
// independently allows the entire loop to auto-vectorize, and is well worth it.
float rowTopX = ctx.topX + (y - ctx.topY) * ctx.xSlope;
float rowBottomX = ctx.topX + (y + 1.0f - ctx.topY) * ctx.xSlope;
processRow<kXDir>(ctx, yIdx, rowTopX, rowBottomX, 0xffffffff,
/*checkStart=*/false, /*checkEnd=*/false);
}
// Process the Terminal Row, if it exists. I.e. if it's on-screen AND wasn't already
// processed as the Top Row.
if (ctx.p1TileY >= yStart && ctx.p1TileY < static_cast<int32_t>(tileRows)) {
float y = static_cast<float>(ctx.p1TileY);
// No guard is necessary here against horizontal lines, as a horizontal line would
// have been processed as a starting row.
float rowTopX = ctx.topX + (y - ctx.topY) * ctx.xSlope;
// No need to check start (we are past it), but must check end.
processRow<kXDir>(ctx, ctx.p1TileY, rowTopX, ctx.bottomX, 0xffffffff,
/*checkStart=*/false, /*checkEnd=*/true);
}
}
};
} // namespace skgpu::graphite
#endif // skgpu_graphite_sparse_strips_Tiler_DEFINED