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