1 /*
2 * Copyright 2022 Google LLC
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #ifndef skgpu_graphite_ClipStack_DEFINED
9 #define skgpu_graphite_ClipStack_DEFINED
10
11 #include "include/core/SkClipOp.h"
12 #include "include/private/base/SkTArray.h"
13 #include "src/base/SkTBlockList.h"
14 #include "src/gpu/graphite/DrawOrder.h"
15 #include "src/gpu/graphite/DrawParams.h"
16 #include "src/gpu/graphite/geom/Shape.h"
17 #include "src/gpu/graphite/geom/Transform.h"
18
19 class SkShader;
20 class SkStrokeRec;
21
22 namespace skgpu::graphite {
23
24 class BoundsManager;
25 class Device;
26 class Geometry;
27
28 // TODO: Port over many of the unit tests for skgpu/v1/ClipStack defined in GrClipStackTest since
29 // those tests do a thorough job of enumerating the different element combinations.
30 class ClipStack {
31 public:
32 // TODO: Some of these states reflect what SkDevice requires. Others are based on what Ganesh
33 // could handle analytically. They will likely change as graphite's clips are sorted out
34 enum class ClipState : uint8_t {
35 kEmpty, kWideOpen, kDeviceRect, kDeviceRRect, kComplex
36 };
37
38 // All data describing a geometric modification to the clip
39 struct Element {
40 Shape fShape;
41 Transform fLocalToDevice; // TODO: reference a cached Transform like DrawList?
42 SkClipOp fOp;
43 };
44
45 // 'owningDevice' must outlive the clip stack.
46 ClipStack(Device* owningDevice);
47
48 ~ClipStack();
49
50 ClipStack(const ClipStack&) = delete;
51 ClipStack& operator=(const ClipStack&) = delete;
52
clipState()53 ClipState clipState() const { return this->currentSaveRecord().state(); }
maxDeferredClipDraws()54 int maxDeferredClipDraws() const { return fElements.count(); }
55 Rect conservativeBounds() const;
56
57 class ElementIter;
58 // Provides for-range over active, valid clip elements from most recent to oldest.
59 // The iterator provides items as "const Element&".
60 inline ElementIter begin() const;
61 inline ElementIter end() const;
62
63 // Clip stack manipulation
64 void save();
65 void restore();
66
67 // The clip stack does not have a notion of AA vs. non-AA. However, if PixelSnapping::kYes is
68 // used and the right conditions are met, it can adjust the clip geometry to align with the
69 // pixel grid and emulate some aspects of non-AA behavior.
70 enum class PixelSnapping : bool {
71 kNo = false,
72 kYes = true
73 };
74 void clipShape(const Transform& localToDevice, const Shape& shape, SkClipOp op,
75 PixelSnapping = PixelSnapping::kNo);
76 void clipShader(sk_sp<SkShader> shader);
77
78 // Compute the bounds and the effective elements of the clip stack when applied to the draw
79 // described by the provided transform, shape, and stroke.
80 //
81 // Applying clips to a draw is a mostly lazy operation except for what is returned:
82 // - The Clip's scissor is set to 'conservativeBounds()'.
83 // - The Clip stores the draw's clipped bounds, taking into account its transform, styling, and
84 // the above scissor.
85 // - The Clip also stores the draw's fill-style invariant clipped bounds which is used in atlas
86 // draws and may differ from the draw bounds.
87 //
88 // All clip elements that affect the draw will be returned in `outEffectiveElements` alongside
89 // the bounds. This method does not have any side-effects and the per-clip element state has to
90 // be explicitly updated by calling `updateClipStateForDraw()` which prepares the clip stack for
91 // later rendering.
92 //
93 // The returned clip element list will be empty if the shape is clipped out or if the draw is
94 // unaffected by any of the clip elements.
95 using ElementList = skia_private::STArray<4, const Element*>;
96 Clip visitClipStackForDraw(const Transform&,
97 const Geometry&,
98 const SkStrokeRec&,
99 bool outsetBoundsForAA,
100 bool msaaSupported,
101 ElementList* outEffectiveElements) const;
102
103 // Update the per-clip element state for later rendering using pre-computed clip state data for
104 // a particular draw. The provided 'z' value is the depth value that the draw will use if it's
105 // not clipped out entirely.
106 //
107 // The returned CompressedPaintersOrder is the largest order that will be used by any of the
108 // clip elements that affect the draw.
109 //
110 // If the provided `clipState` indicates that the draw will be clipped out, then this method has
111 // no effect and returns DrawOrder::kNoIntersection.
112 CompressedPaintersOrder updateClipStateForDraw(const Clip& clip,
113 const ElementList& effectiveElements,
114 const BoundsManager*,
115 PaintersDepth z);
116
117 void recordDeferredClipDraws();
118
119 private:
120 // SaveRecords and Elements are stored in two parallel stacks. The top-most SaveRecord is the
121 // active record, older records represent earlier save points and aren't modified until they
122 // become active again. Elements may be owned by the active SaveRecord, in which case they are
123 // fully mutable, or they may be owned by a prior SaveRecord. However, Elements from both the
124 // active SaveRecord and older records can be valid and affect draw operations. Elements are
125 // marked inactive when new elements are determined to supersede their effect completely.
126 // Inactive elements of the active SaveRecord can be deleted immediately; inactive elements of
127 // older SaveRecords may become active again as the save stack is popped back.
128 //
129 // See go/grclipstack-2.0 for additional details and visualization of the data structures.
130 class SaveRecord;
131
132 // Internally, a lot of clip reasoning is based on an op, outer bounds, and whether a shape
133 // contains another (possibly just conservatively based on inner/outer device-space bounds).
134 // Element and SaveRecord store this information directly. A draw is equivalent to a clip
135 // element with the intersection op. TransformedShape is a lightweight wrapper that can convert
136 // these different types into a common type that Simplify() can reason about.
137 struct TransformedShape;
138 // This captures which of the two elements in (A op B) would be required when they are combined,
139 // where op is intersect or difference.
140 enum class SimplifyResult {
141 kEmpty,
142 kAOnly,
143 kBOnly,
144 kBoth
145 };
146 static SimplifyResult Simplify(const TransformedShape& a, const TransformedShape& b);
147
148 // Wraps the geometric Element data with logic for containment and bounds testing.
149 class RawElement : public Element {
150 public:
151 using Stack = SkTBlockList<RawElement, 1>;
152
153 RawElement(const Rect& deviceBounds,
154 const Transform& localToDevice,
155 const Shape& shape,
156 SkClipOp op,
157 PixelSnapping);
158
~RawElement()159 ~RawElement() {
160 // A pending draw means the element affects something already recorded, so its own
161 // shape needs to be recorded as a draw. Since recording requires the Device (and
162 // DrawContext), it must happen before we destroy the element itself.
163 SkASSERT(!this->hasPendingDraw());
164 }
165
166 // Silence warnings about implicit copy ctor/assignment because we're declaring a dtor
167 RawElement(const RawElement&) = default;
168 RawElement& operator=(const RawElement&) = default;
169
170 operator TransformedShape() const;
171
hasPendingDraw()172 bool hasPendingDraw() const { return fOrder != DrawOrder::kNoIntersection; }
shape()173 const Shape& shape() const { return fShape; }
localToDevice()174 const Transform& localToDevice() const { return fLocalToDevice; }
outerBounds()175 const Rect& outerBounds() const { return fOuterBounds; }
innerBounds()176 const Rect& innerBounds() const { return fInnerBounds; }
op()177 SkClipOp op() const { return fOp; }
178 ClipState clipType() const;
179
180 // As new elements are pushed on to the stack, they may make older elements redundant.
181 // The old elements are marked invalid so they are skipped during clip application, but may
182 // become active again when a save record is restored.
isInvalid()183 bool isInvalid() const { return fInvalidatedByIndex >= 0; }
184 void markInvalid(const SaveRecord& current);
185 void restoreValid(const SaveRecord& current);
186
187 // 'added' represents a new op added to the element stack. Its combination with this element
188 // can result in a number of possibilities:
189 // 1. The entire clip is empty (signaled by both this and 'added' being invalidated).
190 // 2. The 'added' op supercedes this element (this element is invalidated).
191 // 3. This op supercedes the 'added' element (the added element is marked invalidated).
192 // 4. Their combination can be represented by a single new op (in which case this
193 // element should be invalidated, and the combined shape stored in 'added').
194 // 5. Or both elements remain needed to describe the clip (both are valid and unchanged).
195 //
196 // The calling element will only modify its invalidation index since it could belong
197 // to part of the inactive stack (that might be restored later). All merged state/geometry
198 // is handled by modifying 'added'.
199 void updateForElement(RawElement* added, const SaveRecord& current);
200
201 // Returns how this element affects the draw after more detailed analysis.
202 enum class DrawInfluence {
203 kNone, // The element does not affect the draw
204 kClipOut, // The element causes the draw shape to be entirely clipped out
205 kIntersect, // The element intersects the draw shape in a complex way
206 };
207 DrawInfluence testForDraw(const TransformedShape& draw) const;
208
209 // Updates usage tracking to incorporate the bounds and Z value for the new draw call.
210 // If this element hasn't affected any prior draws, it will use the bounds manager to
211 // assign itself a compressed painters order for later rendering.
212 //
213 // This method assumes that this element affects the draw in a complex way, such that
214 // calling `testForDraw()` on the same draw would return `DrawInfluence::kIntersect`. It is
215 // assumed that `testForDraw()` was called beforehand to ensure that this is the case.
216 //
217 // Assuming that this element does not clip out the draw, returns the painters order the
218 // draw must sort after.
219 CompressedPaintersOrder updateForDraw(const BoundsManager* boundsManager,
220 const Rect& drawBounds,
221 PaintersDepth drawZ);
222
223 // Record a depth-only draw to the given device, restricted to the portion of the clip that
224 // is actually required based on prior recorded draws. Resets usage tracking for subsequent
225 // passes.
226 void drawClip(Device*);
227
228 void validate() const;
229
230 private:
231 // TODO: Should only combine elements within the same save record, that don't have pending
232 // draws already. Otherwise, we're changing the geometry that will be rasterized and it
233 // could lead to gaps even if in a perfect the world the analytically intersected shape was
234 // equivalent. Can't combine with other save records, since they *might* become pending
235 // later on.
236 bool combine(const RawElement& other, const SaveRecord& current);
237
238 // Device space bounds. These bounds are not snapped to pixels with the assumption that if
239 // a relation (intersects, contains, etc.) is true for the bounds it will be true for the
240 // rasterization of the coordinates that produced those bounds.
241 Rect fInnerBounds;
242 Rect fOuterBounds;
243 // TODO: Convert fOuterBounds to a ComplementRect to make intersection tests faster?
244 // Would need to store both original and complement, since the intersection test is
245 // Rect + ComplementRect and Element/SaveRecord could be on either side of operation.
246
247 // State tracking how this clip element needs to be recorded into the draw context. As the
248 // clip stack is applied to additional draws, the clip's Z and usage bounds grow to account
249 // for it; its compressed painter's order is selected the first time a draw is affected.
250 Rect fUsageBounds;
251 CompressedPaintersOrder fOrder;
252 PaintersDepth fMaxZ;
253
254 // Elements are invalidated by SaveRecords as the record is updated with new elements that
255 // override old geometry. An invalidated element stores the index of the first element of
256 // the save record that invalidated it. This makes it easy to undo when the save record is
257 // popped from the stack, and is stable as the current save record is modified.
258 int fInvalidatedByIndex;
259 };
260
261 // Represents a saved point in the clip stack, and manages the life time of elements added to
262 // stack within the record's life time. Also provides the logic for determining active elements
263 // given a draw query.
264 class SaveRecord {
265 public:
266 using Stack = SkTBlockList<SaveRecord, 2>;
267
268 explicit SaveRecord(const Rect& deviceBounds);
269
270 SaveRecord(const SaveRecord& prior, int startingElementIndex);
271
shader()272 const SkShader* shader() const { return fShader.get(); }
outerBounds()273 const Rect& outerBounds() const { return fOuterBounds; }
innerBounds()274 const Rect& innerBounds() const { return fInnerBounds; }
op()275 SkClipOp op() const { return fStackOp; }
276 ClipState state() const;
277 uint32_t genID() const;
278
firstActiveElementIndex()279 int firstActiveElementIndex() const { return fStartingElementIndex; }
oldestElementIndex()280 int oldestElementIndex() const { return fOldestValidIndex; }
canBeUpdated()281 bool canBeUpdated() const { return (fDeferredSaveCount == 0); }
282
283 Rect scissor(const Rect& deviceBounds, const Rect& drawBounds) const;
284
285 // Deferred save manipulation
pushSave()286 void pushSave() {
287 SkASSERT(fDeferredSaveCount >= 0);
288 fDeferredSaveCount++;
289 }
290 // Returns true if the record should stay alive. False means the ClipStack must delete it
popSave()291 bool popSave() {
292 fDeferredSaveCount--;
293 SkASSERT(fDeferredSaveCount >= -1);
294 return fDeferredSaveCount >= 0;
295 }
296
297 // Return true if the element was added to 'elements', or otherwise affected the save record
298 // (e.g. turned it empty).
299 bool addElement(RawElement&& toAdd, RawElement::Stack* elements, Device*);
300
301 void addShader(sk_sp<SkShader> shader);
302
303 // Remove the elements owned by this save record, which must happen before the save record
304 // itself is removed from the clip stack. Records draws for any removed elements that have
305 // draw usages.
306 void removeElements(RawElement::Stack* elements, Device*);
307
308 // Restore element validity now that this record is the new top of the stack.
309 void restoreElements(RawElement::Stack* elements);
310
311 private:
312 // These functions modify 'elements' and element-dependent state of the record
313 // (such as valid index and fState). Records draws for any clips that have deferred usages
314 // that are inactivated and cannot be restored (i.e. part of the active save record).
315 bool appendElement(RawElement&& toAdd, RawElement::Stack* elements, Device*);
316 void replaceWithElement(RawElement&& toAdd, RawElement::Stack* elements, Device*);
317
318 // Inner bounds is always contained in outer bounds, or it is empty. All bounds will be
319 // contained in the device bounds.
320 Rect fInnerBounds; // Inside is full coverage (stack op == intersect) or 0 cov (diff)
321 Rect fOuterBounds; // Outside is 0 coverage (op == intersect) or full cov (diff)
322
323 // A save record can have up to one shader, multiple shaders are automatically blended
324 sk_sp<SkShader> fShader;
325
326 const int fStartingElementIndex; // First element owned by this save record
327 int fOldestValidIndex; // Index of oldest element that's valid for this record
328 int fDeferredSaveCount; // Number of save() calls without modifications (yet)
329
330 // Will be kIntersect unless every valid element is kDifference, which is significant
331 // because if kDifference then there is an implicit extra outer bounds at the device edges.
332 SkClipOp fStackOp;
333 ClipState fState;
334 uint32_t fGenID;
335 };
336
337 Rect deviceBounds() const;
338
currentSaveRecord()339 const SaveRecord& currentSaveRecord() const {
340 SkASSERT(!fSaves.empty());
341 return fSaves.back();
342 }
343
344 // Will return the current save record, properly updating deferred saves
345 // and initializing a first record if it were empty.
346 SaveRecord& writableSaveRecord(bool* wasDeferred);
347
348 RawElement::Stack fElements;
349 SaveRecord::Stack fSaves; // always has one wide open record at the top
350
351 Device* fDevice; // the device this clip stack is coupled with
352 };
353
354 // Clip element iteration
355 class ClipStack::ElementIter {
356 public:
357 bool operator!=(const ElementIter& o) const {
358 return o.fItem != fItem && o.fRemaining != fRemaining;
359 }
360
361 const Element& operator*() const { return *fItem; }
362
363 ElementIter& operator++() {
364 // Skip over invalidated elements
365 do {
366 fRemaining--;
367 ++fItem;
368 } while(fRemaining > 0 && (*fItem).isInvalid());
369
370 return *this;
371 }
372
ElementIter(RawElement::Stack::CRIter::Item item,int r)373 ElementIter(RawElement::Stack::CRIter::Item item, int r) : fItem(item), fRemaining(r) {}
374
375 RawElement::Stack::CRIter::Item fItem;
376 int fRemaining;
377
378 friend class ClipStack;
379 };
380
begin()381 ClipStack::ElementIter ClipStack::begin() const {
382 if (this->currentSaveRecord().state() == ClipState::kEmpty ||
383 this->currentSaveRecord().state() == ClipState::kWideOpen) {
384 // No visible clip elements when empty or wide open
385 return this->end();
386 }
387 int count = fElements.count() - this->currentSaveRecord().oldestElementIndex();
388 return ElementIter(fElements.ritems().begin(), count);
389 }
390
end()391 ClipStack::ElementIter ClipStack::end() const {
392 return ElementIter(fElements.ritems().end(), 0);
393 }
394
395 } // namespace skgpu::graphite
396
397 #endif // skgpu_graphite_ClipStack_DEFINED
398