/* * Copyright 2022 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_ClipStack_DEFINED #define skgpu_graphite_ClipStack_DEFINED #include "include/core/SkClipOp.h" #include "include/private/base/SkTArray.h" #include "src/base/SkTBlockList.h" #include "src/gpu/graphite/DrawOrder.h" #include "src/gpu/graphite/DrawParams.h" #include "src/gpu/graphite/geom/Shape.h" #include "src/gpu/graphite/geom/Transform_graphite.h" class SkShader; class SkStrokeRec; namespace skgpu::graphite { class BoundsManager; class Device; class Geometry; // TODO: Port over many of the unit tests for skgpu/v1/ClipStack defined in GrClipStackTest since // those tests do a thorough job of enumerating the different element combinations. class ClipStack { public: // TODO: Some of these states reflect what SkDevice requires. Others are based on what Ganesh // could handle analytically. They will likely change as graphite's clips are sorted out enum class ClipState : uint8_t { kEmpty, kWideOpen, kDeviceRect, kDeviceRRect, kComplex }; // All data describing a geometric modification to the clip struct Element { Shape fShape; Transform fLocalToDevice; // TODO: reference a cached Transform like DrawList? SkClipOp fOp; }; // 'owningDevice' must outlive the clip stack. ClipStack(Device* owningDevice); ~ClipStack(); ClipStack(const ClipStack&) = delete; ClipStack& operator=(const ClipStack&) = delete; ClipState clipState() const { return this->currentSaveRecord().state(); } int maxDeferredClipDraws() const { return fElements.count(); } Rect conservativeBounds() const; class ElementIter; // Provides for-range over active, valid clip elements from most recent to oldest. // The iterator provides items as "const Element&". inline ElementIter begin() const; inline ElementIter end() const; // Clip stack manipulation void save(); void restore(); // The clip stack does not have a notion of AA vs. non-AA. However, if PixelSnapping::kYes is // used and the right conditions are met, it can adjust the clip geometry to align with the // pixel grid and emulate some aspects of non-AA behavior. enum class PixelSnapping : bool { kNo = false, kYes = true }; void clipShape(const Transform& localToDevice, const Shape& shape, SkClipOp op, PixelSnapping = PixelSnapping::kNo); void clipShader(sk_sp shader); // Compute the bounds and the effective elements of the clip stack when applied to the draw // described by the provided transform, shape, and stroke. // // Applying clips to a draw is a mostly lazy operation except for what is returned: // - The Clip's scissor is set to 'conservativeBounds()'. // - The Clip stores the draw's clipped bounds, taking into account its transform, styling, and // the above scissor. // - The Clip also stores the draw's fill-style invariant clipped bounds which is used in atlas // draws and may differ from the draw bounds. // // All clip elements that affect the draw will be returned in `outEffectiveElements` alongside // the bounds. This method does not have any side-effects and the per-clip element state has to // be explicitly updated by calling `updateClipStateForDraw()` which prepares the clip stack for // later rendering. // // The returned clip element list will be empty if the shape is clipped out or if the draw is // unaffected by any of the clip elements. using ElementList = skia_private::STArray<4, const Element*>; Clip visitClipStackForDraw(const Transform&, const Geometry&, const SkStrokeRec&, bool outsetBoundsForAA, ElementList* outEffectiveElements) const; // Update the per-clip element state for later rendering using pre-computed clip state data for // a particular draw. The provided 'z' value is the depth value that the draw will use if it's // not clipped out entirely. // // The returned CompressedPaintersOrder is the largest order that will be used by any of the // clip elements that affect the draw. // // If the provided `clipState` indicates that the draw will be clipped out, then this method has // no effect and returns DrawOrder::kNoIntersection. CompressedPaintersOrder updateClipStateForDraw(const Clip& clip, const ElementList& effectiveElements, const BoundsManager*, PaintersDepth z); void recordDeferredClipDraws(); private: // SaveRecords and Elements are stored in two parallel stacks. The top-most SaveRecord is the // active record, older records represent earlier save points and aren't modified until they // become active again. Elements may be owned by the active SaveRecord, in which case they are // fully mutable, or they may be owned by a prior SaveRecord. However, Elements from both the // active SaveRecord and older records can be valid and affect draw operations. Elements are // marked inactive when new elements are determined to supersede their effect completely. // Inactive elements of the active SaveRecord can be deleted immediately; inactive elements of // older SaveRecords may become active again as the save stack is popped back. // // See go/grclipstack-2.0 for additional details and visualization of the data structures. class SaveRecord; // Internally, a lot of clip reasoning is based on an op, outer bounds, and whether a shape // contains another (possibly just conservatively based on inner/outer device-space bounds). // Element and SaveRecord store this information directly. A draw is equivalent to a clip // element with the intersection op. TransformedShape is a lightweight wrapper that can convert // these different types into a common type that Simplify() can reason about. struct TransformedShape; // This captures which of the two elements in (A op B) would be required when they are combined, // where op is intersect or difference. enum class SimplifyResult { kEmpty, kAOnly, kBOnly, kBoth }; static SimplifyResult Simplify(const TransformedShape& a, const TransformedShape& b); // Wraps the geometric Element data with logic for containment and bounds testing. class RawElement : public Element { public: using Stack = SkTBlockList; RawElement(const Rect& deviceBounds, const Transform& localToDevice, const Shape& shape, SkClipOp op, PixelSnapping); ~RawElement() { // A pending draw means the element affects something already recorded, so its own // shape needs to be recorded as a draw. Since recording requires the Device (and // DrawContext), it must happen before we destroy the element itself. SkASSERT(!this->hasPendingDraw()); } // Silence warnings about implicit copy ctor/assignment because we're declaring a dtor RawElement(const RawElement&) = default; RawElement& operator=(const RawElement&) = default; operator TransformedShape() const; bool hasPendingDraw() const { return fOrder != DrawOrder::kNoIntersection; } const Shape& shape() const { return fShape; } const Transform& localToDevice() const { return fLocalToDevice; } const Rect& outerBounds() const { return fOuterBounds; } const Rect& innerBounds() const { return fInnerBounds; } SkClipOp op() const { return fOp; } ClipState clipType() const; // As new elements are pushed on to the stack, they may make older elements redundant. // The old elements are marked invalid so they are skipped during clip application, but may // become active again when a save record is restored. bool isInvalid() const { return fInvalidatedByIndex >= 0; } void markInvalid(const SaveRecord& current); void restoreValid(const SaveRecord& current); // 'added' represents a new op added to the element stack. Its combination with this element // can result in a number of possibilities: // 1. The entire clip is empty (signaled by both this and 'added' being invalidated). // 2. The 'added' op supercedes this element (this element is invalidated). // 3. This op supercedes the 'added' element (the added element is marked invalidated). // 4. Their combination can be represented by a single new op (in which case this // element should be invalidated, and the combined shape stored in 'added'). // 5. Or both elements remain needed to describe the clip (both are valid and unchanged). // // The calling element will only modify its invalidation index since it could belong // to part of the inactive stack (that might be restored later). All merged state/geometry // is handled by modifying 'added'. void updateForElement(RawElement* added, const SaveRecord& current); // Returns how this element affects the draw after more detailed analysis. enum class DrawInfluence { kNone, // The element does not affect the draw kClipOut, // The element causes the draw shape to be entirely clipped out kIntersect, // The element intersects the draw shape in a complex way }; DrawInfluence testForDraw(const TransformedShape& draw) const; // Updates usage tracking to incorporate the bounds and Z value for the new draw call. // If this element hasn't affected any prior draws, it will use the bounds manager to // assign itself a compressed painters order for later rendering. // // This method assumes that this element affects the draw in a complex way, such that // calling `testForDraw()` on the same draw would return `DrawInfluence::kIntersect`. It is // assumed that `testForDraw()` was called beforehand to ensure that this is the case. // // Assuming that this element does not clip out the draw, returns the painters order the // draw must sort after. CompressedPaintersOrder updateForDraw(const BoundsManager* boundsManager, const Rect& drawBounds, PaintersDepth drawZ); // Record a depth-only draw to the given device, restricted to the portion of the clip that // is actually required based on prior recorded draws. Resets usage tracking for subsequent // passes. void drawClip(Device*); void validate() const; private: // TODO: Should only combine elements within the same save record, that don't have pending // draws already. Otherwise, we're changing the geometry that will be rasterized and it // could lead to gaps even if in a perfect the world the analytically intersected shape was // equivalent. Can't combine with other save records, since they *might* become pending // later on. bool combine(const RawElement& other, const SaveRecord& current); // Device space bounds. These bounds are not snapped to pixels with the assumption that if // a relation (intersects, contains, etc.) is true for the bounds it will be true for the // rasterization of the coordinates that produced those bounds. Rect fInnerBounds; Rect fOuterBounds; // TODO: Convert fOuterBounds to a ComplementRect to make intersection tests faster? // Would need to store both original and complement, since the intersection test is // Rect + ComplementRect and Element/SaveRecord could be on either side of operation. // State tracking how this clip element needs to be recorded into the draw context. As the // clip stack is applied to additional draws, the clip's Z and usage bounds grow to account // for it; its compressed painter's order is selected the first time a draw is affected. Rect fUsageBounds; CompressedPaintersOrder fOrder; PaintersDepth fMaxZ; // Elements are invalidated by SaveRecords as the record is updated with new elements that // override old geometry. An invalidated element stores the index of the first element of // the save record that invalidated it. This makes it easy to undo when the save record is // popped from the stack, and is stable as the current save record is modified. int fInvalidatedByIndex; }; // Represents a saved point in the clip stack, and manages the life time of elements added to // stack within the record's life time. Also provides the logic for determining active elements // given a draw query. class SaveRecord { public: using Stack = SkTBlockList; explicit SaveRecord(const Rect& deviceBounds); SaveRecord(const SaveRecord& prior, int startingElementIndex); const SkShader* shader() const { return fShader.get(); } const Rect& outerBounds() const { return fOuterBounds; } const Rect& innerBounds() const { return fInnerBounds; } SkClipOp op() const { return fStackOp; } ClipState state() const; int firstActiveElementIndex() const { return fStartingElementIndex; } int oldestElementIndex() const { return fOldestValidIndex; } bool canBeUpdated() const { return (fDeferredSaveCount == 0); } Rect scissor(const Rect& deviceBounds, const Rect& drawBounds) const; // Deferred save manipulation void pushSave() { SkASSERT(fDeferredSaveCount >= 0); fDeferredSaveCount++; } // Returns true if the record should stay alive. False means the ClipStack must delete it bool popSave() { fDeferredSaveCount--; SkASSERT(fDeferredSaveCount >= -1); return fDeferredSaveCount >= 0; } // Return true if the element was added to 'elements', or otherwise affected the save record // (e.g. turned it empty). bool addElement(RawElement&& toAdd, RawElement::Stack* elements, Device*); void addShader(sk_sp shader); // Remove the elements owned by this save record, which must happen before the save record // itself is removed from the clip stack. Records draws for any removed elements that have // draw usages. void removeElements(RawElement::Stack* elements, Device*); // Restore element validity now that this record is the new top of the stack. void restoreElements(RawElement::Stack* elements); private: // These functions modify 'elements' and element-dependent state of the record // (such as valid index and fState). Records draws for any clips that have deferred usages // that are inactivated and cannot be restored (i.e. part of the active save record). bool appendElement(RawElement&& toAdd, RawElement::Stack* elements, Device*); void replaceWithElement(RawElement&& toAdd, RawElement::Stack* elements, Device*); // Inner bounds is always contained in outer bounds, or it is empty. All bounds will be // contained in the device bounds. Rect fInnerBounds; // Inside is full coverage (stack op == intersect) or 0 cov (diff) Rect fOuterBounds; // Outside is 0 coverage (op == intersect) or full cov (diff) // A save record can have up to one shader, multiple shaders are automatically blended sk_sp fShader; const int fStartingElementIndex; // First element owned by this save record int fOldestValidIndex; // Index of oldest element that's valid for this record int fDeferredSaveCount; // Number of save() calls without modifications (yet) // Will be kIntersect unless every valid element is kDifference, which is significant // because if kDifference then there is an implicit extra outer bounds at the device edges. SkClipOp fStackOp; ClipState fState; }; Rect deviceBounds() const; const SaveRecord& currentSaveRecord() const { SkASSERT(!fSaves.empty()); return fSaves.back(); } // Will return the current save record, properly updating deferred saves // and initializing a first record if it were empty. SaveRecord& writableSaveRecord(bool* wasDeferred); RawElement::Stack fElements; SaveRecord::Stack fSaves; // always has one wide open record at the top Device* fDevice; // the device this clip stack is coupled with }; // Clip element iteration class ClipStack::ElementIter { public: bool operator!=(const ElementIter& o) const { return o.fItem != fItem && o.fRemaining != fRemaining; } const Element& operator*() const { return *fItem; } ElementIter& operator++() { // Skip over invalidated elements do { fRemaining--; ++fItem; } while(fRemaining > 0 && (*fItem).isInvalid()); return *this; } ElementIter(RawElement::Stack::CRIter::Item item, int r) : fItem(item), fRemaining(r) {} RawElement::Stack::CRIter::Item fItem; int fRemaining; friend class ClipStack; }; ClipStack::ElementIter ClipStack::begin() const { if (this->currentSaveRecord().state() == ClipState::kEmpty || this->currentSaveRecord().state() == ClipState::kWideOpen) { // No visible clip elements when empty or wide open return this->end(); } int count = fElements.count() - this->currentSaveRecord().oldestElementIndex(); return ElementIter(fElements.ritems().begin(), count); } ClipStack::ElementIter ClipStack::end() const { return ElementIter(fElements.ritems().end(), 0); } } // namespace skgpu::graphite #endif // skgpu_graphite_ClipStack_DEFINED