| /* |
| * 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_StripProcessorScalar_DEFINED |
| #define skgpu_graphite_sparse_strips_StripProcessorScalar_DEFINED |
| |
| #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/Tiler.h" |
| |
| #include <cstdint> |
| |
| namespace skgpu::graphite { |
| |
| template <uint16_t kTileWidth, uint16_t kTileHeight, bool kIsWinding> |
| class StripProcessorScalar { |
| public: |
| StripProcessorScalar(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->clearWindingForNewRow(); |
| } |
| |
| SK_ALWAYS_INLINE void clearWinding(int16_t value) { |
| for (int32_t row = 0; row < kTileHeight; ++row) { |
| for (int32_t column = 0; column < kTileWidth; ++column) { |
| for (int32_t k = 0; k < Strip::kNumSubSamples; ++k) { |
| fSubsampleWinding[row][column][k] = value; |
| } |
| } |
| } |
| } |
| |
| SK_ALWAYS_INLINE void clearWithCoarseWinding() { |
| this->clearWinding(static_cast<int16_t>(fCoarseWinding)); |
| } |
| |
| SK_ALWAYS_INLINE void clearWindingForNewRow() { this->clearWinding(0); } |
| |
| SK_ALWAYS_INLINE static bool ShouldFill(int32_t w) { |
| if constexpr (kIsWinding) { |
| return w != 0; // NonZero |
| } else { |
| return (w & 1) != 0; // EvenOdd |
| } |
| } |
| |
| 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; } |
| |
| // Once all tiles at the same geometric location have been combined, the subsample winding must |
| // be converted to alpha values consumable by the fragment shader. |
| SK_ALWAYS_INLINE void resolveWindingToAlpha() { |
| uint8_t* tileAlphaBase = this->reserveAlphaBuffer(); |
| int localWriteIdx = 0; |
| |
| for (int32_t row = 0; row < kTileHeight; ++row) { |
| for (int32_t column = 0; column < kTileWidth; ++column) { |
| this->processPixel(row, column, tileAlphaBase, localWriteIdx++); |
| } |
| } |
| fLocalAlphaIdx += kTilePixelCount; |
| } |
| |
| // The core function of strip generation. It takes the line, coarse winding, and intersection |
| // mask and converts it to subsample winding. There are two core techniques that make MSAA |
| // calculation on the CPU feasible: |
| // |
| // 1. LUT Lookup (Sub-pixel Coverage) |
| // Evaluating the line equation to determine winding at 8 or 16 subsample locations per |
| // pixel is prohibitively expensive on the CPU. Instead, a fixed set of subsample coverage |
| // patterns are precomputed into a look-up table. |
| // |
| // 2. Hierarchical Winding: A naive scanline renderer at 8xMSAA and a 4x4 tile would require |
| // carrying 4 x 8 = 32 scanlines across the tiles. Instead, the winding is calculated |
| // *hierarchically*: the "coarse winding" is only carried at the top left corner of the tile |
| // and each subsample reconstructs its winding from that point using the intersected line. |
| // |
| // 2x2 Tile 8xMSAA |
| // +----------------+----------------+ C +----------------+----------------+ |
| // |-o--------------|-o-> | | | o | o | |
| // |------------o---|------------o-> | v | o | o | |
| // |--------o-------|--------o-> | | | o | o | |
| // |---------------o|---------------o| v | o| o| |
| // |---o------------|---o-> | | | o | o | |
| // |---------o------|---------o-> | v | o | o | |
| // |-------------o--|-------------o->| | | o | o | |
| // |-----o----------|-----o-> | v | o | o | |
| // +----------------+----------------+ | +----------------+----------------+ |
| // |-o--------------|-o-> | v | o | o | |
| // |------------o---|------------o-> | | | o | o | |
| // |--------o-------|--------o-> | v | o | o | |
| // |---------------o|---------------o| | | o| o| |
| // |---o------------|---o-> | v | o | o | |
| // |---------o------|---------o-> | | | o | o | |
| // |-------------o--|-------------o->| v | o | o | |
| // |-----o----------|-----o-> | └-|---->o | o | |
| // +----------------+----------------+ +----------------+----------------+ |
| // Naive Scanline Hierarchical |
| // |
| // ------------------------------------------------------------------------------------------ |
| // Rasterize Line To Tile Conceptual Flow: |
| // Although this is not the logical flow of the function, it is helpful to think of the |
| // function itself as having a "Coarse Phase," which calculates the "aliased winding" for each |
| // pixel which is not intersected by the line, and a "Fine Phase," which calculates |
| // "antialiased winding" for pixels intersected by the line. |
| // |
| // Coarse Phase: |
| // 0. Coarse Winding |
| // The overall winding state is tracked hierarchically, anchored at the top-left corner of |
| // the tile. When processing begins for a new spatial tile, this "backdrop" coarse winding |
| // is used to "seed" the initial subsample winding. Tiles rasterized at the same spatial |
| // location continue to accumulate their winding contributions. This accumulation does not |
| // alter the current tile's backdrop, but instead computes the net winding at the top-right |
| // corner of the spatial tile, which in turn becomes the top-left backdrop for the next tile |
| // in the row. |
| // |
| // 1. Crossing Top & Propagating Right |
| // As the line moves vertically through a tile, standard scanline rules apply: crossing the |
| // top edge of a pixel dictates that all pixels to the right of the crossing pixel have |
| // their winding toggled. |
| // |
| // Crossed top at X = 0 |
| // | |
| // V |
| // +------X---------+----------------+ |
| // | o * | # | |
| // | * o | # | <--Pixels X > 0 filled |
| // | * o | # | |
| // | * o| #| |
| // | o * | # | |
| // | * o | # | |
| // | * o | # | |
| // | o* | # | |
| // +------X---------+----------------+ |
| // | o * | # | |
| // | * o | # | |
| // | * o | # | |
| // | * o| #| |
| // | o * | # | |
| // | * o | # | |
| // | * o | # | |
| // | o* | # | |
| // +------X---------+----------------+ |
| // |
| // 2. Fill Left |
| // Because lines do not necessarily span the entire height of the tile, the "Crossing Top" |
| // rule alone is insufficient as it does not consider winding effects below the line. So it |
| // is supplemented with the "Fill Left" rule: if a line segment crosses the left boundary |
| // of the tile, we toggle all pixels below that edge. |
| // |
| // +---------X------+----------------+ |
| // | o * | o | |
| // | * o | o | |
| // | * o | o | |
| // | * o| o| |
| // Crosses left --> X o | o | |
| // inside pixel Y = 0 | o | o | |
| // | o | o | |
| // | o | o | |
| // +----------------+----------------+ |
| // | # | # | |
| // | # | # | |
| // | # | # | |
| // | #| #| |
| // | # | # | <-- Pixels Y > 0 filled |
| // | # | # | |
| // | # | # | |
| // | # | # | |
| // +----------------+----------------+ |
| // |
| // ------------------------------------------------------------------------------------------ |
| // Fine Phase: |
| // 0. Per Row Intersections: |
| // The intersection of the line and each row edge is found, using the tile-edge |
| // intersections as the first and last entry. |
| // |
| // Already given by ClipToTile |
| // | |
| // V |
| // +--X-------------+----------------+ |
| // | o * | o | |
| // | * o | o | |
| // | * o | o | |
| // | * o| o| |
| // | o * | o | |
| // | o | o | |
| // | * o | o | |
| // | o * | o | |
| // +-------------X--+----------------+ |
| // | o * | o | |
| // | o *| o | |
| // | o * o | |
| // | o|* o| |
| // | o | * o | |
| // | o | * o | |
| // | o | * o | |
| // | o | o | |
| // +----------------+-----X----------+ |
| // ^ |
| // | |
| // Already given by ClipToTile |
| // |
| // 1. LUT Lookup |
| // The LUT is indexed by a normalized slope and parametric `t`. Because the line is |
| // invariant across the pixels of the tiles, we perform an initial setup--- |
| // calculateLineStepParams()---before processing pixels. This produces the normalized slope |
| // and initial parametric `t` (tBase) value. Instead of evaluating the line equation at each |
| // pixel and recalculating `t`, we instead also calculate parametric step values that map to |
| // traversing right and down, then DDA step `tBase` as we move through the tile. See |
| // calculateLineStepParams() for more details. |
| // |
| // +--X-------------+----------------+ |
| // | # * | o | |
| // | * o | o | |
| // | * o | o | |
| // | * o| o| |
| // | # * | o | |
| // | # | o | |
| // | * o | o | |
| // | # * | o | |
| // +-------------X--+----------------+ |
| // | # * | o | |
| // | # *| o | |
| // | # * o | |
| // | #|* o| |
| // | # | * o | |
| // | # | * o | |
| // | # | * o | |
| // | # | # | |
| // +----------------+-----X----------+ |
| // |
| // 2. Truncation |
| // Note: "Terminates" and "endpoint" are used to describe the start *or* end of the line. |
| // |
| // Because the LUT is indexed only by `t` and the slope, it always projects the line from |
| // `t` to the opposite edge of the pixel. This implicitly assumes that the line contributes |
| // winding to the entire vertical span it covers. |
| // |
| // This assumption fails when a line terminates within the interior of a pixel, or directly |
| // on a right pixel edge. Projecting such lines past their true endpoints creates a "shadow |
| // region" where winding is either double-counted or fails to destruct correctly. |
| // |
| // To correct this, we use truncation: a process where we mask out the LUT subsamples that |
| // fall either above or below the Y plane of the line endpoint. Which half is ignored |
| // depends on the line's y-direction. Because winding follows clockwise or counterclockwise |
| // winding rules, this direction dictates whether the interior of the polygon shape is above |
| // or below that point. |
| // |
| // A) Endpoints inside the pixel: Because paths are watertight, a sibling line always shares |
| // an endpoint with the current line. Without truncation, the projected "shadow" of the |
| // lines extends past their shared endpoint, resulting in double-counted or non-destructed |
| // winding. Truncating the projection at the shared endpoint's y-plane eliminates this |
| // shadow region. |
| // |
| // x: Line 1 *: Line 2 |
| // | | |
| // V V |
| // +----------------+----------------+ |
| // Projected Shadow --> | o x *| o | <-- No Winding from 2 to |
| // | x # * | o | destruct in Shadow |
| // #: Line 1 Winding -> | x # * | o | |
| // | x * D| o| |
| // Shared Endpoint --> | o x | o | <-- D: Winding from 1 and 2 |
| // | * x D | o | destructs correctly |
| // | * x D | o | |
| // | * s x | o | <-- s: Line 2 Winding |
| // +----------------+----------------+ |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o| o| |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // +----------------+----------------+ |
| // |
| // A) With truncation: |
| // |
| // +----------------+----------------+ |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o| o| |
| // Truncation Plane --> |---o---x--------| o | |
| // | * x D | o | |
| // | * x D | o | |
| // | * s x | o | |
| // +----------------+----------------+ |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o| o| |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // +----------------+----------------+ |
| // |
| // B) Endpoints on a right pixel edge: Pixel traversal (like tile traversal) is vertically |
| // exclusive but horizontally inclusive: a line ending exactly on a pixel's right edge |
| // still processes the adjacent pixel to the right. Following the LUT rules, it projects |
| // the line across that adjacent pixel. While carrying the winding into the next pixel is |
| // the correct behavior, it carries the same risk as interior endpoints if not truncated: |
| // |
| // *: Line 1 x: Line 2 |
| // | | |
| // V V |
| // +---------X------X----------------+ |
| // | o * x D | |
| // #: Line 1 Winding -> | # x D | |
| // | o * x D | |
| // | #x D| <-- D: Winding from 1 and 2 |
| // | o x* D | destructs |
| // | o x * D | |
| // | o x * D | |
| // s: Line 2 Winding -> | o x s* | <-- No Winding from 1 to |
| // +----------------+----------------+ destruct in Shadow |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o| o| |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // +----------------+----------------+ |
| // |
| // B) With truncation: |
| // |
| // +---------X------X----------------+ |
| // | o * x D | |
| // | # x D | |
| // | o * x D | |
| // | #x---------------D| <-- Truncation plane |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // +----------------+----------------+ |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o| o| |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // | o | o | |
| // +----------------+----------------+ |
| // |
| // 3. Truncation for Left-Edge Touches |
| // For pixel-aligned left-edge touches (e.g., Y = 1.0): |
| // A) Bottom endpoint: Vertical exclusivity means processing stops *before* |
| // row Y = 1. Its winding contribution comes entirely from fillLeft(). |
| // B) Top endpoint: No truncation is needed; rule (A) guarantees no sibling |
| // pixel contribution can exist. |
| // NOTE: This logic relies on integer Y-coordinates being directly representable |
| // in f32 (which is guaranteed up to 16,777,216, or 2^24). |
| // |
| // Fractional left-edge touches (e.g., Y = 1.15) must handle when the line continues into |
| // the adjacent left tile, and when the line terminates on the edge. These cases are |
| // disambiguated using a combination of the Tiler's left-bit rules and the clipping logic. |
| // |
| // Tiler Left-Bit Rules For Tiles Containing Line Endpoints: |
| // +----------------------+----------+------------+ |
| // | 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 | |
| // +----------------------+----------+------------+ |
| // |
| // ClipToTile: ClipToTile guarantees that the intersection points it returns are sorted top |
| // to bottom. Crucially, it also returns whether the left intersection, if any, was at the |
| // top or bottom point in the tile---`topIsOnLeftEdge`, `botIsOnLeftEdge`. |
| // |
| // Group 1: Continuous Line (No Truncation on Left Edge) |
| // A) Single Line: The line exists on both sides of the tile edge, meaning that the |
| // line should contribute winding to the entire pixel. Thus no truncation is |
| // required. The corresponding `*leftEdge` flag will always be true. |
| // |
| // Group 2: Edge Terminal Vertices (Truncation Required) |
| // The line terminates on the left edge. The winding contribution only exists below or |
| // above the touch point, depending on the y-direction. |
| // B) Terminates on Edge, Sibling in Same Tile: One sibling must have a R->L end and |
| // the other must have a L->R start. No L-Bit is set for either tile, triggering |
| // truncation for both. |
| // |
| // C) Terminates on Edge, Sibling in Left Tile: Due to the horizontal inclusive |
| // traversal of tiles, a line that ends exactly on the right edge of a tile produces |
| // a single point "grazing touch" tile to its right. This tile must always truncate; |
| // in either x-direction the grazing tile will contain the L-bit (because it must be |
| // a L->R end or R->L start). Normally the L-bit would preclude truncation, but in |
| // this case, the graze produces a single point intersection, where the top and |
| // bottom pixel are the same, thus both truncation masks apply. The line terminating |
| // on the left edge never has an L-bit (it must be a L->R start or a R->L end), so |
| // it always truncates. |
| // |
| // D) Double Grazing Touch: Sometimes, both lines end on a right edge, producing two |
| // grazing tiles. Both must have L-bits. Like case (C), one of the `*leftEdge` flags |
| // must evaluate to `false`, triggering truncation for the shadows of both lines. |
| // |
| // 4. Winding and Coverage Inversion |
| // The mask returned by the LUT is entirely pixel-local; it only represents which subsamples |
| // fall on the "positive" side of the line equation within the pixel. This is always the |
| // result of the equivalent left-right scanline crossing (e.g., the positive side is always |
| // to the right of the crossing). Instead, the winding in the mask is corrected using |
| // "coverage inversion." |
| // |
| // We must also account for Clockwise (CW) vs. Counterclockwise (CCW) winding directions |
| // when using the NonZero fill rule. (The EvenOdd rule simply computes parity via XOR). For |
| // the NonZero rule, direction determines the sign of the winding number (1/-1): |
| // A) Simple Paths: For an isolated, non-self-intersecting shape, CW/CCW produce the |
| // same mask but with opposite winding. Because any NonZero value resolves as |
| // "filled", both windings produce the same visual output. |
| // |
| // B) Complex Paths & Holes: Multiple paths in the same scene can interact with each |
| // other and produce different geometry based on CW/CCW. An inner sub-path drawn in |
| // the opposite direction of its outer container will destruct winding numbers |
| // (e.g., 1 - 1 = 0), cutting a hole. If drawn in the same direction, they |
| // accumulate (e.g., 1 + 1 = 2), keeping the area filled. |
| // |
| // Thus, for any pixel intersected by the line, its winding contribution *to that local |
| // tile* is given by `canonicalYDir * (LUT - invert)` where: |
| // A) `LUT` is the value of a single subsample location (e.g., 0 or 1). |
| // |
| // B) `canonicalYDir` determines the *direction* of the winding contribution (e.g., 1 |
| // or -1). Again, note that for the EvenOdd fill rule, this is unnecessary, as the |
| // result of an XOR is equivalent regardless of direction. |
| // |
| // C) `invert` is a boolean that essentially acts as a toggle: if false, the LUT's |
| // mask is used as-is. If true, the mask is inverted. Note that for the NonZero fill |
| // rule, subtraction is not the equivalent of XOR (i.e., (0 ^ 1) != 0 - 1). |
| // |
| // There are two rules for coverage inversion: |
| // A) `startInvert = pTopY != floor && topIsOnLeftEdge`: applies only to the entrant |
| // or starting pixel in the tile. |
| // |
| // B) `defaultInvert = pTopY != floor && sortedXDir`: applies to all other pixels. |
| // |
| // Both inversion rules share the `pTopY != floor` parameter, which has two effects: |
| // A) fillLeft applies to pixel rows above `ceil(pLeftIntersectionY)`. If pTopY is |
| // pixel-aligned (i.e., `pTopY == floor`), then fillLeft already applies to the |
| // top row, requiring no correction. |
| // |
| // B) A secondary implication of this rule is that `pTopY == floor` will always be |
| // false for any row other than the topmost; thus, it is evaluated against all pixel |
| // rows to elide branching. |
| // |
| // Due to ClipToTile sorting: |
| // A) `topLeftIsOnEdge` is only true when the topmost point in the tile is on the left |
| // edge |
| // B) `sortedXDir` is *only true when the implicitly descending (e.g. pTopY->pBotY)* |
| // line is right travelling. |
| // Thus, it is useful to first think about coverage inversion in terms of a simple downward-right |
| // sloping line contained in a single row. It can start interior to the row: |
| // |
| // +----------------+----------------+----------------+----------------+ |
| // | | | | | |
| // | * | | | | |
| // | | * | | | |
| // | | * | | | |
| // | | | * | | |
| // | | | * | | |
| // | | | | * | |
| // | | | | *| |
| // +----------------+----------------+----------------+----------------+ |
| // |
| // Or on the left edge: |
| // |
| // +----------------+----------------+----------------+----------------+ |
| // X* | | | | |
| // | * | | | | |
| // | |* | | | |
| // | | * | | | |
| // | | | * | | |
| // | | | * | | |
| // | | | | * | |
| // | | | | * | |
| // +----------------+----------------+----------------+----------------+ |
| // |
| // This line can either be on the top edge of the geometry, implying a CW path direction, or |
| // on the bottom edge, implying CCW. Regardless of the path direction, the mask inversion |
| // should always follow the Right-Hand Rule on the equivalent CW path, with CW/CCW only |
| // affecting the winding direction. Thus, if a line is: |
| // |
| // A) On the top edge of the geometry: the fill should be below the line. However, |
| // a right-descending line causes the LUT to place the fill *above* the line, thus |
| // requiring correction. The ascending case requires no correction, as the LUT correctly |
| // places the filled mask below the line. |
| // |
| // For the descending case, the mask subtracts `invert`, resulting in 0/1 -> -1/0. |
| // Since the line is descending, canonicalYDir = 1, resulting in -1/0 * 1 = -1/0. Since |
| // the path must be CW, the interior must be negative, matching the LUT. |
| // |
| // TODO(thomsmit): explain the needle case here too? |
| // |
| // B) On the bottom edge of the geometry: A right-descending line correctly places the |
| // fill above the line, with the correct winding direction. (The path must be CCW, so the |
| // interior region should have positive winding). However, a line at the bottom edge |
| // does not try to *fill* the region with the correct winding; it must instead destruct |
| // the winding that fills the interior, which comes from: |
| // 1) Backdrop Winding: If the line touches the left edge and there is a sibling in |
| // this tile, the top-left corner of the tile must be in the interior, seeding the |
| // entire tile. |
| // 2) fillLeft from a non-touching sibling: If the line touches the left edge, and a |
| // sibling *is* in the tile, then that sibling may contribute a fillLeft. If the |
| // pixel intersection is above the row, this overlaps and destructs. |
| // 3) crossTop: If the line does not touch the left edge, its sibling may cross the |
| // top edge of the shared touch pixel. This contributes a crossTop. |
| // 4) Non-touching sibling needle: The sibling may enter the exact same row from the |
| // left, or it may touch but not cross. (In the touch-left case, the fill does not |
| // hit this row). The sibling either ascends or requires its own correction. |
| // |
| // In all four cases, the contributed winding *must* be positive: |
| // 1) If backdrop: because it is CCW, it must be positive. |
| // 2) If fillLeft: because it is CCW, the sibling line must be moving left, so it must |
| // be positive. |
| // 3) If crossTop: because it is CCW, it must be positive. |
| // 4) In the needle case: the sibling is either ascending naturally (contributing |
| // positive winding below its line) or is corrected and multiplied by -1 (thereby |
| // contributing positive winding). |
| // |
| // In general, lines at the top and left edges of polygon cast winding shadows; lines at the |
| // bottom and right edges destruct with those shadows. Thus, we *always* want 0 winding |
| // above the line, and negative winding beneath the line. Consequently, the desired result |
| // is the same regardless of which edge (L/R, B/T, which is effectively just CW/CCW) the |
| // line falls on, meaning the same rules apply to both. |
| // |
| // Special Case: Perfectly Horizontal Line |
| // Fractional horizontal lines (which bypass the pixel-aligned early-out) are treated as |
| // descending. This happens because the `canonicalYDir` check uses greater than or *equal |
| // to* (`p1.fY >= p0.fY`). As a result, these horizontal lines are treated as the |
| // right-descending lines described above. |
| // |
| // 5. Coverage Inversion and Truncation |
| // Up until this point, we have not discussed how inversion interacts with truncation. To |
| // reiterate: truncation masks away vertical spans, while inversion correction shifts |
| // winding. Because this interaction can be mutually exclusive based on which pixel in the |
| // top row is being processed, we use two variables `startInvert` and `defaultInvert` to |
| // differentiate the cases: |
| // |
| // A) `startInvert` (The topmost pixel in the top row of the tile.) |
| // For the first pixel a line segment touches in the tile, truncation and inversion are |
| // mutually exclusive. Truncation at the start of a line masks out the area *above* the |
| // line's starting Y-plane. Because inversion correction already forces winding below the |
| // line, truncating that top area is redundant. Thus, the starting pixel either corrects |
| // or truncates, but never both: |
| // |
| // -- Left-Edge Touching: A line starting on the left edge (`topIsOnLeftEdge`) applies |
| // inversion but skips truncation, casting its winding into the pixel. A descending |
| // line terminating on the left edge (`!topIsOnLeftEdge`) must truncate to handle its |
| // sibling's contribution; it skips inversion, relying on the sibling to place the |
| // winding on the correct side. |
| // |
| // -- Interior Terminating: If the start is in the interior (`!topIsOnLeftEdge`), it must |
| // always truncate. Just like the left-edge terminating case, it relies on the sibling |
| // pixel's contribution for correct mask toggling, so inversion is skipped. |
| // |
| // B) `defaultInvert` (All other pixels in the top row.) |
| // For the remaining pixels in the top row, inversion and truncation are *not* mutually |
| // exclusive. Because inversion pushes winding below the line, it can interact with |
| // *bottom* truncation: |
| // |
| // -- Continuing Pixels: These simply apply inversion and are not truncated. |
| // |
| // -- Ending Pixels: If a segment is completely contained within the topmost row, its |
| // final pixel receives *both* `defaultInvert` correction and truncation. The fill |
| // is corrected to 0/-1 beneath the line, and because it is an end pixel, truncation |
| // masks away the area *below* the pixel's ending Y-plane. This forms a triangle |
| // region bounded by the truncation plane, the line, and the left pixel edge. |
| // |
| // Special Case: Double Grazing Touch |
| // A "double grazing touch" occurs when two sibling lines meet exactly on the right edge of |
| // an adjacent tile, producing two single-point "grazing touches" on the left edge of the |
| // current tile. Because they meet at a single point, the clipping/tiler logic guarantees |
| // one segment evaluates `topIsOnLeftEdge` to true, and the other to false. Thanks to the |
| // mutual exclusivity of `startInvert`: |
| // -- The segment evaluating to `true` applies inversion but no truncation, casting the |
| // winding shadow. |
| // -- The segment evaluating to `false` applies truncation but no inversion, masking |
| // the shadow. |
| // |
| 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; |
| |
| // Accumulate the coarse winding for the next spatial tile, fSubsampleWinding has already |
| // been seeded during tile state transition. |
| 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; |
| |
| // Since the coordinates for the left edge touch are tile local, i.e. [0.0, TileHeightF], |
| // a perfectly pixel aligned left edge intersection e.g. y == 1.0 is guaranteed to be |
| // directly representable by f32. Thus guarantees that calling ceil(leftEdgeIntersectionY), |
| // and calling floor() in the sidedness calculations can be used to determine perfect |
| // pixel alignment. |
| if (tile.hasLeftIntersection()) { |
| float yEdge = pTop.fX < pBot.fX ? pTop.fY : pBot.fY; |
| this->fillLeft(yEdge, canonicalXDir); |
| } |
| |
| // Ignore perfectly horizontal lines that lie exactly on pixel boundaries, as their winding |
| // contribution is accounted for by fillLeft() |
| if (std::abs(dy) < Strip::kStripEpsilon && pTop.fY == std::floor(pTop.fY)) { |
| return; |
| } |
| |
| // Use the tile intersection points to get the vertical span in (integer) pixels |
| int32_t startY = static_cast<int32_t>(std::floor(pTop.fY)); |
| int32_t endY = static_cast<int32_t>(std::ceil(pBot.fY)); |
| |
| // Find x-intersection for each pixel row; rows which have no intersection will return NaN |
| auto rowInt = FindRowIntersections(pTop, pBot, dxdy, startY, endY); |
| LineStepParams stepParams = this->computeLineStepParams(pTop, pBot, dx, dy); |
| |
| // For each intersected pixel row, determine the "fine" coverage. |
| for (int32_t row = startY; row < endY; ++row) { |
| float pTopX = rowInt[row].fX; |
| float pTopY = rowInt[row].fY; |
| float pBotX = rowInt[row + 1].fX; |
| float pBotY = rowInt[row + 1].fY; |
| |
| // This should never happen, the SIMD implementation handles through loop peeling |
| if (std::isnan(pTopX) || std::isnan(pBotX)) { |
| continue; |
| } |
| |
| // Determine horizontal pixel span for this specific row. |
| 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 normalized translation parameter `tVal` at the starting pixel |
| // (xStart, row) of the span using the linear projection formula: |
| // t(x, y) = tBase + stepY * y + stepX * x |
| float tVal = stepParams.fTBase + stepParams.fStepY * static_cast<float>(row) + |
| stepParams.fStepX * static_cast<float>(xStart); |
| |
| bool isStartY = (row == startY); |
| bool isEndYMinus1 = (row == endY - 1); |
| |
| bool crossedTop = pTopY == std::floor(pTopY); |
| bool defaultInvert = !crossedTop && stepParams.fSortedXDir; |
| |
| int canonicalStartX = stepParams.fSortedXDir ? xStart : xEnd; |
| int canonicalEndX = stepParams.fSortedXDir ? xEnd : xStart; |
| |
| // The Look-Up Table (LUT) assumes that lines always fully traverse a pixel. However, if |
| // the line terminates inside a pixel, start and end masks must be used to truncate the |
| // line's winding contribution. For a detailed explanation on *why* truncation is |
| // necessary, and *when* we truncate, see section 2 above. For details on the mechanics |
| // of truncation itself, see GetTruncationMask() below. |
| uint8_t startMask = 0xff; |
| bool startInvert = defaultInvert; |
| if (isStartY) { |
| startInvert = (topIsOnLeftEdge && !crossedTop); |
| if (!topIsOnLeftEdge) { |
| // Truncate subsamples above the segment's starting y. |
| startMask = |
| GetTruncationMask</*kIsStart=*/true>(pTopY, static_cast<float>(row)); |
| } |
| } |
| |
| uint8_t endMask = 0xff; |
| if (isEndYMinus1 && !botIsOnLeftEdge) { |
| // Truncate subsamples below the segment's ending y. |
| endMask = |
| GetTruncationMask</*kIsStart=*/false>(pBotY, static_cast<float>(row)); |
| } |
| |
| // Iterate left-to-right through the pixels in the current row. |
| for (int32_t column = xStart; column <= xEnd; ++column) { |
| uint8_t maskVal = this->lutLookup(tVal, stepParams.fLutRow); |
| |
| // Apply the truncation masks if we are at the geometric ends of the segment. |
| uint8_t edgeMask = 0xff; |
| if (column == canonicalStartX) edgeMask &= startMask; |
| if (column == canonicalEndX) edgeMask &= endMask; |
| maskVal &= edgeMask; |
| |
| // Correct when LUT places the fill on the incorrect side of the line. Note: The |
| // lutMask may already be in the correct configuration, requiring no adjustment. The |
| // logic for the top pixel is is different. See section 4 above. |
| bool shouldInvert = (isStartY && column == canonicalStartX) ? startInvert |
| : defaultInvert; |
| |
| for (int32_t k = 0; k < Strip::kNumSubSamples; ++k) { |
| // Extract the 1 or 0 from the LUT for this specific sub-sample. |
| int32_t maskBit = (maskVal & (1 << k)) ? 1 : 0; |
| |
| // Accumulate the corrected mask bit into the tile's subsample winding. |
| if constexpr (kIsWinding) { |
| // For NonZero rule, the mask *subtracts* the correction. |
| maskBit -= shouldInvert; |
| if (maskBit != 0) { |
| if (canonicalYDir) { |
| fSubsampleWinding[row][column][k] += maskBit; |
| } else { |
| fSubsampleWinding[row][column][k] -= maskBit; |
| } |
| } |
| } else { |
| // EvenOdd rule just uses XOR. |
| fSubsampleWinding[row][column][k] ^= (maskBit ^ shouldInvert); |
| } |
| } |
| |
| // Step to the next column: since the X coordinate increases by 1.0, incrementally |
| // add stepX to the translation parameter `tVal` (DDA). |
| tVal += stepParams.fStepX; |
| } |
| |
| // Crossing top |
| if (crossedTop) { |
| int32_t val; |
| if constexpr (kIsWinding) { |
| val = canonicalYDir ? 1 : -1; |
| } else { |
| val = 1; |
| } |
| for (int32_t column = xEnd + 1; column < kTileWidth; ++column) { |
| for (int32_t k = 0; k < Strip::kNumSubSamples; ++k) { |
| if constexpr (kIsWinding) { |
| fSubsampleWinding[row][column][k] += val; |
| } else { |
| fSubsampleWinding[row][column][k] ^= val; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private: |
| static constexpr int32_t kTilePixelCount = kTileWidth * kTileHeight; |
| |
| struct LineStepParams { |
| int fLutRow; |
| float fStepX; |
| float fStepY; |
| float fTBase; |
| bool fSortedXDir; |
| }; |
| |
| 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); |
| } |
| |
| // For testing, we need to know the fill rule result for each subsample location, not just the |
| // summed alpha that we store in the alpha buffer. So on testing builds, we store the exact |
| // results in a mask for each pixel in the tile. |
| #if defined(GPU_TEST_UTILS) |
| SK_ALWAYS_INLINE void observePixel(int32_t row, int32_t column) { |
| uint8_t exactMask = 0; |
| for (int32_t k = 0; k < Strip::kNumSubSamples; ++k) { |
| if (ShouldFill(fSubsampleWinding[row][column][k])) { |
| exactMask |= (1 << k); |
| } |
| } |
| if (fIsInverse) { |
| exactMask = ~exactMask & ((1 << Strip::kNumSubSamples) - 1); |
| } |
| fObserver(exactMask); |
| } |
| #endif |
| |
| // To convert fSubsampleWinding to alpha, we naively iterate through each subsample location and |
| // check the winding against the fill rule. In the SIMD version, this is done more effeciently. |
| // Inverse fills, are handled by inverting the coverage. |
| SK_ALWAYS_INLINE void processPixel(int32_t row, |
| int32_t column, |
| uint8_t* tileAlphaBase, |
| int32_t localWriteIdx) { |
| int32_t activeSamples = 0; |
| |
| for (int32_t k = 0; k < Strip::kNumSubSamples; ++k) { |
| if (ShouldFill(fSubsampleWinding[row][column][k])) { |
| activeSamples++; |
| } |
| } |
| if (fIsInverse) { |
| activeSamples = Strip::kNumSubSamples - activeSamples; |
| } |
| |
| #if defined(GPU_TEST_UTILS) |
| if (fObserver) { |
| this->observePixel(row, column); |
| } |
| #endif |
| |
| uint8_t alpha = static_cast<uint8_t>((activeSamples * 255 + (Strip::kNumSubSamples / 2)) / |
| Strip::kNumSubSamples); |
| tileAlphaBase[localWriteIdx] = alpha; |
| } |
| |
| // Account for the winding contribution from a line line in the same row, but which has not |
| // crossed the top edge of its tile. |
| SK_ALWAYS_INLINE void fillLeft(float yEdge, bool canonicalXDir) { |
| int32_t startY = static_cast<int32_t>(std::ceil(yEdge)); |
| |
| int32_t val; |
| if constexpr (kIsWinding) { |
| val = canonicalXDir ? -1 : 1; |
| } else { |
| val = 1; |
| } |
| |
| for (int32_t row = startY; row < kTileHeight; ++row) { |
| for (int32_t column = 0; column < kTileWidth; ++column) { |
| for (int32_t k = 0; k < Strip::kNumSubSamples; ++k) { |
| if constexpr (kIsWinding) { |
| fSubsampleWinding[row][column][k] += val; |
| } else { |
| fSubsampleWinding[row][column][k] ^= val; |
| } |
| } |
| } |
| } |
| } |
| |
| SK_ALWAYS_INLINE static std::array<SkPoint, kTileHeight + 1> FindRowIntersections( |
| SkPoint pTop, SkPoint pBot, float dxdy, int32_t startY, int32_t endY) { |
| std::array<SkPoint, kTileHeight + 1> rowInt; |
| rowInt.fill(SkPoint::Make(std::numeric_limits<float>::quiet_NaN(), |
| std::numeric_limits<float>::quiet_NaN())); |
| rowInt[startY] = pTop; |
| for (int32_t row = startY + 1; row < endY; ++row) { |
| float gy = static_cast<float>(row); |
| float gx = pTop.fX + (gy - pTop.fY) * dxdy; |
| rowInt[row] = {gx, gy}; |
| } |
| rowInt[endY] = pBot; |
| return rowInt; |
| } |
| |
| // When truncating, we mask away subsamples *above* the topmost point or subsamples *below* |
| // bottommost point. (These can be the raw line endpoint or a tile edge intersection). To do |
| // this, we rely on the properties of our LUT mask construction: |
| // |
| // 1. N-Rooks Subsample Pattern: The D3D11 subsample pattern is intentionally skewed so that no |
| // two points share the same X or Y coordinate. For MSAAx8, each of the 8 subsamples |
| // occupies its own distinct 1/8th vertical slice of the pixel (MSAA_LUT.h:L130), so the |
| // fractional vertical distance inside the pixel maps linearly to the number of subsamples. |
| // Thus, multiplying the fractional Y coordinate by 8.0f (e.g., 8.0f * (pTopY - float(row))) |
| // directly yields the correct bit shift amount. If samples shared the same Y plane, mapping |
| // distance to a shift amount would require manual correction to the shift amount. |
| // |
| // 2. Vertically Sorted Subsamples: The subsample mask bits are ordered bottom-to-top, with the |
| // LSB corresponding to the topmost subsample point in the pixel. Because they are sorted |
| // sequentially by their Y-coordinates, bitshifting left/right correctly corresponds to |
| // truncating top/bottom. |
| // |
| // Mechanically, truncation works by bitwise ANDing a start or end mask against the LUT's |
| // output. To generate these masks, we start with a fully covered mask (e.g., `0xff` for MSAAx8) |
| // and shift it based on the point's vertical position inside the pixel: |
| // `shift = round(8.0f * (p - float(row)))`. |
| // |
| // Because our tile traversal is top-to-bottom, `row` is always the Y-coordinate of the top edge |
| // of the current row. Therefore, `p - row` gives the fractional distance from the top edge down |
| // to point `p`. |
| // |
| // Rounding is a convenient trick that converts that fractional distance into an integer shift |
| // amount. Because the subsample locations are placed at pixel centers (MSAA_LUT.h:L130), a |
| // fractional distance > .5 must be crossing (and thus truncated) and vice versa. This maps |
| // almost exactly to semantics of rounding, enabling this conversion to be completed in a single |
| // hardware instruction. A side effect of this is that lines which end precisely at vertical |
| // pixel midpoints (e.g. n.5), will roundup and truncate, but this is visually inconsequential |
| // and well worth the performance gain. |
| // |
| // Thus, combining properties 1 and 2, left-shifting a full mask (e.g., `0xff` for MSAAx8) by |
| // the shift amount masks out the samples *above* point `p` to create the start mask. |
| // |
| // For the end mask, we perform the same fractional distance calculation against the bottom |
| // point (e.g. finding the distance above the point), but after left-shifting `0xff`, we simply |
| // bitwise NOT (`~`) the result, returning the mask of the points below the line endpoint. |
| // |
| // TODO(thomsmit): microoptimization, possibly rewrite this to put the branch on the |
| // assignment not the actual mask calculation? |
| 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)); |
| } |
| } |
| |
| /* |
| * Computes the normalized parameter space (s, t) LineStepParams used to index into the |
| * precomputed MSAA look-up table: |
| * |
| * 0. DDA |
| * In theory, to find the LUT column 't' for any pixel (X, Y), we could evaluate the full |
| * line equation from scratch. In practice, this is slow. Because the line is straight, the |
| * change 't' as we move 1 pixel right (X + 1) or 1 pixel down (Y + 1) is constant. This |
| * allows us to use a approach: t(X, Y) = tBase + (X * stepX) + (Y * stepY). Thus, we |
| * calculate `tBase`, `stepX`, and `stepY` once upfront, and then step `t` from there. |
| * |
| * 1. Normalizing the Slope, Manhattan Distance: |
| * The LUT construnction normalizes slopes into a bounded (0, 1] parameter using: `s = 1 / |
| * (m + 1)`, where `m = |dy/dx|`. However, since the construction operates directly on `s`, |
| * it does not need to account for vertical lines (e.g. dx = 0). But when constructing `s` |
| * organically, we do need to handle this. So instead, we multiply the numerator and the |
| * denominator by `dx`, resulting in: `s = |dx| / (|dy| + |dx|)` Notice that the denominator |
| * is exactly the Manhattan distance `D` of the line's rightward-pointing normal vector: |
| * D = normalX + |normalY|. Thus, we compute: `s = |normalY| / D` |
| * |
| * 2. DDA Stepping (stepX and stepY): |
| * These represent the change in `t` as we step from pixel to pixel within the tile. |
| * Expanding the original half plane parameterization: (x - (1 - t))(1 - s) - (y - t)s >= 0, |
| * we can see that: |
| * -- Moving 1 pixel right (x + 1) changes the value by exactly (1 - s). So |
| * 1 - (|normalY| / D) = normalX / D. So |
| * stepX = normalX * invManhattanDistance. |
| * -- Moving 1 pixel down (y + 1) changes the value by exactly -s. So |
| * normalY / D. (If dy > 0, normalY is negative, yielding -s). So |
| * stepY = normalY * invManhattanDistance. |
| * |
| * 3. tBase: |
| * To use incremental stepping, we need a starting point. `tBase` calculates the |
| * normalized translation, 't', at the origin (0, 0) (i.e. the top left of the tile): |
| * -- The unnormalized line offset at the origin is `-localLineConstant`. |
| * -- For positive slopes, we shift this by `normalX` to align the phase with the LUT. |
| * -- For negative slopes, we shift the Y-intercept by |normalY| because LUT generation |
| * flips the Y-axis, `(y = 1.0 - y)` to elide branching on the half plane equation. |
| * Note: the lut is still divided into positive and negative portions. |
| * TODO (thomsmit): adds complexity without performance improvement? Possibility |
| * remove this? |
| * We compensate by phase-shifting the offset by the full Manhattan distance. Finally, this |
| * offset is multiplied by invManhattanDistance to map it into the [0.0, 1.0] bounds of the |
| * LUT's horizontal 'u' axis. |
| * |
| * Note: |
| * 1. While these parameters could be cached per line, empirical results show that it is faster |
| * to simply rematerialize them. |
| * 2. In practice, we offset by stepY per row, and then stepX across the row. Although it would |
| * also be correct to simply stepY to the next row, recalculating limits the amount of fp |
| * error that can accumulate. |
| */ |
| SK_ALWAYS_INLINE LineStepParams computeLineStepParams(SkPoint pTop, SkPoint pBot, |
| float dx, float dy) const { |
| float normalX = dy; |
| float normalY = -dx; |
| |
| // Force the normal vector to always point right (positive X). This ensures the returned LUT |
| // mask emulates scanline left->right scanline behavior. |
| if (normalX < 0.0f) { |
| normalX = -normalX; |
| normalY = -normalY; |
| } |
| |
| // Compute the Manhattan distance of the normal vector. |
| float D = normalX + std::abs(normalY); |
| float invD = (D < Strip::kStripEpsilon) ? 0.0f : 1.0f / D; |
| |
| // In screen-space (Y goes down), a line that goes right-and-down (\) yields a negative |
| // normalY, which maps to a positive slope. |
| bool hasPositiveSlope = normalY <= 0.0f; |
| |
| // Compute the constant in the implicit line equation: Ax + By = C. |
| float C = normalX * pTop.fX + normalY * pTop.fY; |
| |
| // Find `s`, and get the row associated with it in the LUT |
| float s = std::abs(normalY) * invD; |
| int lutRowOffset = std::clamp( |
| static_cast<int>(std::floor(s * (Strip::kLutMaskHeight / 2))), |
| 0, |
| (Strip::kLutMaskHeight / 2) - 1); |
| |
| // If the slope is positive, shift the index into the bottom half of the LUT. |
| int lutRow = hasPositiveSlope ? (lutRowOffset + Strip::kLutMaskHeight / 2) : lutRowOffset; |
| |
| // DDA Steps |
| float stepX = normalX * invD; |
| float stepY = normalY * invD; |
| |
| // Calculate the `tBase` at the top left of the tile. Substituting y = 1 - y' into the line |
| // equation changes the translation offset from -C to (D - C). Thus, tBase uses `normalX` |
| // for positive slopes and `D` for negative slopes. See `tBase` section above. |
| float tBase = ((hasPositiveSlope ? normalX : D) - C) * invD; |
| |
| // NOTE: This is NOT the canonicalXDir, this is the direction of the top to bottom points. |
| bool sortedXDir = pTop.fX <= pBot.fX; |
| |
| return {lutRow, stepX, stepY, tBase, sortedXDir}; |
| } |
| |
| // Fetch the subsample coverage mask from the pre-computed Look-Up Table (LUT) based on the |
| // line's calculated trajectory (u, v). `u` is the column index, obtained by scaling the |
| // translation parameter `t` (which ranges from 0.0 to 1.0) to the LUT's width (64) and flooring |
| // the result. |
| SK_ALWAYS_INLINE uint8_t lutLookup(float t, int lutRow) { |
| int u = std::clamp(static_cast<int>(std::floor(t * Strip::kLutMaskWidthF)), |
| 0, |
| Strip::kLutMaskWidthExcl); |
| int index = lutRow * Strip::kLutMaskWidth + u; |
| SkASSERT(index < fMaskLut.size()); |
| return fMaskLut[index]; |
| } |
| |
| // Holds the raw accumulated winding, per subsample per pixel, for the spatial tile currently |
| // being processed. |
| int16_t fSubsampleWinding[kTileHeight][kTileWidth][Strip::kNumSubSamples]; |
| // Integer winding at the top left corner of the tile. |
| int32_t fCoarseWinding; |
| // Buffer to accumulate the generated strips. |
| SkTDArray<Strip>* fStripBuf; |
| // Buffer to accumulate the generated alpha values. |
| SkTDArray<uint8_t>* fAlphaBuf; |
| // Toggles inverse winding behavior. |
| bool fIsInverse; |
| // Reference to the polyline container which holds the flattened paths |
| const Polyline& fPolyline; |
| // Reference to the slope intercept lookup table used to evaluate subsample winding. |
| const SkTDArray<uint8_t>& fMaskLut; |
| // Alpha index, independent of the contents of the alpha buffer. Separated for atlasing. |
| int32_t fLocalAlphaIdx; |
| #if defined(GPU_TEST_UTILS) |
| // Hook, used to test the correctness of the coverage generation. |
| MsaaExactMaskObserver fObserver; |
| #endif |
| }; |
| |
| } // namespace skgpu::graphite |
| |
| #endif // skgpu_graphite_sparse_strips_StripProcessorScalar_DEFINED |