ccpr: Clip path octo bounds by the scissor

Finds the (octagonal) intersection of the path's bounding octagon and
the scissor, and draws that octagon instead. This allows us to avoid
ever using the scissor when drawing paths to the main canvas. It will
also let us use that same octagon without scissor when resolving the
stencil buffer to coverage in MSAA mode.

Bug: skia:
Change-Id: Ia7fe60343424bc77532fa9919d3fa108337a5d63
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/212840
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
This commit is contained in:
Chris Dalton 2019-05-13 08:57:53 -06:00 committed by Skia Commit-Bot
parent cd734f6539
commit a6fcb761d8
7 changed files with 361 additions and 36 deletions

View File

@ -151,6 +151,7 @@ tests_sources = [
"$_tests/MetaDataTest.cpp",
"$_tests/MipMapTest.cpp",
"$_tests/NonlinearBlendingTest.cpp",
"$_tests/OctoBoundsTest.cpp",
"$_tests/OSPathTest.cpp",
"$_tests/OffsetSimplePolyTest.cpp",
"$_tests/OnFlushCallbackTest.cpp",

View File

@ -108,9 +108,15 @@ GrCCDrawPathsOp::GrCCDrawPathsOp(const SkMatrix& m, const GrShape& shape, float
paint.getColor4f())
, fProcessors(std::move(paint)) { // Paint must be moved after fetching its color above.
SkDEBUGCODE(fBaseInstance = -1);
// FIXME: intersect with clip bounds to (hopefully) improve batching.
// (This is nontrivial due to assumptions in generating the octagon cover geometry.)
this->setBounds(conservativeDevBounds, GrOp::HasAABloat::kYes, GrOp::IsZeroArea::kNo);
// If the path is clipped, CCPR will only draw the visible portion. This helps improve batching,
// since it eliminates the need for scissor when drawing to the main canvas.
// FIXME: We should parse the path right here. It will provide a tighter bounding box for us to
// give the opList, as well as enabling threaded parsing when using DDL.
SkRect clippedDrawBounds;
if (!clippedDrawBounds.intersect(conservativeDevBounds, SkRect::Make(maskDevIBounds))) {
clippedDrawBounds.setEmpty();
}
this->setBounds(clippedDrawBounds, GrOp::HasAABloat::kYes, GrOp::IsZeroArea::kNo);
}
GrCCDrawPathsOp::~GrCCDrawPathsOp() {

View File

@ -365,26 +365,33 @@ GrCCAtlas* GrCCPerFlushResources::renderShapeInAtlas(
stroke.getJoin(), stroke.getMiter(), stroke.getCap(), strokeDevWidth);
octoBounds->outset(r);
}
octoBounds->roundOut(devIBounds);
GrScissorTest scissorTest;
SkIRect clippedPathIBounds;
if (!this->placeRenderedPathInAtlas(clipIBounds, *devIBounds, &scissorTest, &clippedPathIBounds,
devToAtlasOffset)) {
GrScissorTest enableScissorInAtlas;
if (clipIBounds.contains(octoBounds->bounds())) {
enableScissorInAtlas = GrScissorTest::kDisabled;
} else if (octoBounds->clip(clipIBounds)) {
enableScissorInAtlas = GrScissorTest::kEnabled;
} else {
// The clip and octo bounds do not intersect. Draw nothing.
SkDEBUGCODE(--fEndPathInstance);
return nullptr; // Path was degenerate or clipped away.
return nullptr;
}
octoBounds->roundOut(devIBounds);
SkASSERT(clipIBounds.contains(*devIBounds));
this->placeRenderedPathInAtlas(*devIBounds, enableScissorInAtlas, devToAtlasOffset);
if (stroke.isFillStyle()) {
SkASSERT(0 == strokeDevWidth);
fFiller.parseDeviceSpaceFill(path, fLocalDevPtsBuffer.begin(), scissorTest,
clippedPathIBounds, *devToAtlasOffset);
fFiller.parseDeviceSpaceFill(path, fLocalDevPtsBuffer.begin(), enableScissorInAtlas,
*devIBounds, *devToAtlasOffset);
} else {
// Stroke-and-fill is not yet supported.
SkASSERT(SkStrokeRec::kStroke_Style == stroke.getStyle() || stroke.isHairlineStyle());
SkASSERT(!stroke.isHairlineStyle() || 1 == strokeDevWidth);
fStroker.parseDeviceSpaceStroke(path, fLocalDevPtsBuffer.begin(), stroke, strokeDevWidth,
scissorTest, clippedPathIBounds, *devToAtlasOffset);
fStroker.parseDeviceSpaceStroke(
path, fLocalDevPtsBuffer.begin(), stroke, strokeDevWidth, enableScissorInAtlas,
*devIBounds, *devToAtlasOffset);
}
return &fRenderedAtlasStack.current();
}
@ -398,41 +405,34 @@ const GrCCAtlas* GrCCPerFlushResources::renderDeviceSpacePathInAtlas(
return nullptr;
}
GrScissorTest scissorTest;
GrScissorTest enableScissorInAtlas;
SkIRect clippedPathIBounds;
if (!this->placeRenderedPathInAtlas(clipIBounds, devPathIBounds, &scissorTest,
&clippedPathIBounds, devToAtlasOffset)) {
if (clipIBounds.contains(devPathIBounds)) {
clippedPathIBounds = devPathIBounds;
enableScissorInAtlas = GrScissorTest::kDisabled;
} else if (clippedPathIBounds.intersect(clipIBounds, devPathIBounds)) {
enableScissorInAtlas = GrScissorTest::kEnabled;
} else {
// The clip and path bounds do not intersect. Draw nothing.
return nullptr;
}
fFiller.parseDeviceSpaceFill(devPath, SkPathPriv::PointData(devPath), scissorTest,
this->placeRenderedPathInAtlas(clippedPathIBounds, enableScissorInAtlas, devToAtlasOffset);
fFiller.parseDeviceSpaceFill(devPath, SkPathPriv::PointData(devPath), enableScissorInAtlas,
clippedPathIBounds, *devToAtlasOffset);
return &fRenderedAtlasStack.current();
}
bool GrCCPerFlushResources::placeRenderedPathInAtlas(const SkIRect& clipIBounds,
const SkIRect& pathIBounds,
GrScissorTest* scissorTest,
SkIRect* clippedPathIBounds,
SkIVector* devToAtlasOffset) {
if (clipIBounds.contains(pathIBounds)) {
*clippedPathIBounds = pathIBounds;
*scissorTest = GrScissorTest::kDisabled;
} else if (clippedPathIBounds->intersect(clipIBounds, pathIBounds)) {
*scissorTest = GrScissorTest::kEnabled;
} else {
return false;
}
void GrCCPerFlushResources::placeRenderedPathInAtlas(
const SkIRect& clippedPathIBounds, GrScissorTest scissorTest, SkIVector* devToAtlasOffset) {
if (GrCCAtlas* retiredAtlas =
fRenderedAtlasStack.addRect(*clippedPathIBounds, devToAtlasOffset)) {
fRenderedAtlasStack.addRect(clippedPathIBounds, devToAtlasOffset)) {
// We did not fit in the previous coverage count atlas and it was retired. Close the path
// parser's current batch (which does not yet include the path we just parsed). We will
// render this batch into the retired atlas during finalize().
retiredAtlas->setFillBatchID(fFiller.closeCurrentBatch());
retiredAtlas->setStrokeBatchID(fStroker.closeCurrentBatch());
}
return true;
}
bool GrCCPerFlushResources::finalize(GrOnFlushResourceProvider* onFlushRP,

View File

@ -122,8 +122,7 @@ public:
private:
void recordCopyPathInstance(const GrCCPathCacheEntry&, const SkIVector& newAtlasOffset,
GrCCPathProcessor::DoEvenOddFill, sk_sp<GrTextureProxy> srcProxy);
bool placeRenderedPathInAtlas(const SkIRect& clipIBounds, const SkIRect& pathIBounds,
GrScissorTest*, SkIRect* clippedPathIBounds,
void placeRenderedPathInAtlas(const SkIRect& clippedPathIBounds, GrScissorTest,
SkIVector* devToAtlasOffset);
const SkAutoSTArray<32, SkPoint> fLocalDevPtsBuffer;

View File

@ -6,8 +6,115 @@
*/
#include "src/gpu/ccpr/GrOctoBounds.h"
#include <algorithm>
bool GrOctoBounds::clip(const SkIRect& clipRect) {
// Intersect dev bounds with the clip rect.
float l = std::max(fBounds.left(), (float)clipRect.left());
float t = std::max(fBounds.top(), (float)clipRect.top());
float r = std::min(fBounds.right(), (float)clipRect.right());
float b = std::min(fBounds.bottom(), (float)clipRect.bottom());
float l45 = fBounds45.left();
float t45 = fBounds45.top();
float r45 = fBounds45.right();
float b45 = fBounds45.bottom();
// Check if either the bounds or 45-degree bounds are empty. We write this check as the NOT of
// non-empty rects, so we will return false if any values are NaN.
if (!(l < r && t < b && l45 < r45 && t45 < b45)) {
return false;
}
// Tighten dev bounds around the new (octagonal) intersection that results after clipping. This
// may be tighter now even than the clipped bounds, depending on the diagonals. Shader code that
// emits octagons expects both bounding boxes to circumcribe the inner octagon, and will fail if
// they do not.
if (l45 > Get_x45(r,b)) {
// Slide the bottom upward until it crosses the l45 diagonal at x=r.
// y = x + (y0 - x0)
// Substitute: l45 = x0 - y0
// y = x - l45
b = SkScalarPin(r - l45, t, b);
} else if (r45 < Get_x45(r,b)) {
// Slide the right side leftward until it crosses the r45 diagonal at y=b.
// x = y + (x0 - y0)
// Substitute: r45 = x0 - y0
// x = y + r45
r = SkScalarPin(b + r45, l, r);
}
if (l45 > Get_x45(l,t)) {
// Slide the left side rightward until it crosses the l45 diagonal at y=t.
// x = y + (x0 - y0)
// Substitute: l45 = x0 - y0
// x = y + l45
l = SkScalarPin(t + l45, l, r);
} else if (r45 < Get_x45(l,t)) {
// Slide the top downward until it crosses the r45 diagonal at x=l.
// y = x + (y0 - x0)
// Substitute: r45 = x0 - y0
// y = x - r45
t = SkScalarPin(l - r45, t, b);
}
if (t45 > Get_y45(l,b)) {
// Slide the left side rightward until it crosses the t45 diagonal at y=b.
// x = -y + (x0 + y0)
// Substitute: t45 = x0 + y0
// x = -y + t45
l = SkScalarPin(t45 - b, l, r);
} else if (b45 < Get_y45(l,b)) {
// Slide the bottom upward until it crosses the b45 diagonal at x=l.
// y = -x + (y0 + x0)
// Substitute: b45 = x0 + y0
// y = -x + b45
b = SkScalarPin(b45 - l, t, b);
}
if (t45 > Get_y45(r,t)) {
// Slide the top downward until it crosses the t45 diagonal at x=r.
// y = -x + (y0 + x0)
// Substitute: t45 = x0 + y0
// y = -x + t45
t = SkScalarPin(t45 - r, t, b);
} else if (b45 < Get_y45(r,t)) {
// Slide the right side leftward until it crosses the b45 diagonal at y=t.
// x = -y + (x0 + y0)
// Substitute: b45 = x0 + y0
// x = -y + b45
r = SkScalarPin(b45 - t, l, r);
}
// Tighten the 45-degree bounding box. Since the dev bounds are now fully tightened, we only
// have to clamp the diagonals to outer corners.
// NOTE: This will not cause l,t,r,b to need more insetting. We only ever change a diagonal by
// pinning it to a FAR corner, which, by definition, is still outside the other corners.
l45 = SkScalarPin(Get_x45(l,b), l45, r45);
t45 = SkScalarPin(Get_y45(l,t), t45, b45);
r45 = SkScalarPin(Get_x45(r,t), l45, r45);
b45 = SkScalarPin(Get_y45(r,b), t45, b45);
// Make one final check for empty or NaN bounds. If the dev bounds were clipped completely
// outside one of the diagonals, they will have been pinned to empty. It's also possible that
// some Infs crept in and turned into NaNs.
if (!(l < r && t < b && l45 < r45 && t45 < b45)) {
return false;
}
fBounds.setLTRB(l, t, r, b);
fBounds45.setLTRB(l45, t45, r45, b45);
#ifdef SK_DEBUG
// Verify dev bounds are inside the clip rect.
SkASSERT(l >= (float)clipRect.left());
SkASSERT(t >= (float)clipRect.top());
SkASSERT(r <= (float)clipRect.right());
SkASSERT(b <= (float)clipRect.bottom());
this->validateBoundsAreTight();
#endif
return true;
}
#if defined(SK_DEBUG) || defined(GR_TEST_UTILS)
void GrOctoBounds::validateBoundsAreTight() const {
this->validateBoundsAreTight([](bool cond, const char* file, int line, const char* code) {
SkASSERTF(cond, "%s(%d): assertion failure: \"assert(%s)\"", file, line, code);

View File

@ -26,12 +26,22 @@
*/
class GrOctoBounds {
public:
GrOctoBounds() = default;
GrOctoBounds(const SkRect& bounds, const SkRect& bounds45) {
this->set(bounds, bounds45);
}
void set(const SkRect& bounds, const SkRect& bounds45) {
fBounds = bounds;
fBounds45 = bounds45;
SkDEBUGCODE(this->validateBoundsAreTight());
}
bool operator==(const GrOctoBounds& that) const {
return fBounds == that.fBounds && fBounds45 == that.fBounds45;
}
bool operator!=(const GrOctoBounds& that) const { return !(*this == that); }
const SkRect& bounds() const { return fBounds; }
float left() const { return fBounds.left(); }
float top() const { return fBounds.top(); }
@ -72,6 +82,13 @@ public:
SkDEBUGCODE(this->validateBoundsAreTight());
}
// Clips the octo bounds by a clip rect and ensures the resulting bounds are fully tightened.
// Returns false if the octagon and clipRect do not intersect at all.
//
// NOTE: Does not perform a trivial containment test before the clip routine. It is probably a
// good idea to not call this method if 'this->bounds()' are fully contained within 'clipRect'.
bool SK_WARN_UNUSED_RESULT clip(const SkIRect& clipRect);
// The 45-degree bounding box resides in "| 1 -1 | * coords" space.
// | 1 1 |
//
@ -84,7 +101,7 @@ public:
constexpr static float Get_x(float x45, float y45) { return (x45 + y45) * .5f; }
constexpr static float Get_y(float x45, float y45) { return (y45 - x45) * .5f; }
#ifdef SK_DEBUG
#if defined(SK_DEBUG) || defined(GR_TEST_UTILS)
void validateBoundsAreTight() const;
void validateBoundsAreTight(const std::function<void(
bool cond, const char* file, int line, const char* code)>& validateFn) const;

195
tests/OctoBoundsTest.cpp Normal file
View File

@ -0,0 +1,195 @@
/*
* Copyright 2019 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/utils/SkRandom.h"
#include "src/gpu/ccpr/GrOctoBounds.h"
#include "tests/Test.h"
using namespace skiatest;
constexpr static float kEpsilon = 1e-3f;
static int numClipsOut = 0;
static int numIntersectClips = 0;
// Ensures devBounds and devBounds45 are valid. Namely, that they are both tight bounding boxes
// around a valid octagon.
static void validate_octo_bounds(
Reporter* reporter, const SkIRect& clipRect, const GrOctoBounds& octoBounds) {
// Verify dev bounds are inside the clip rect.
REPORTER_ASSERT(reporter, octoBounds.left() >= (float)clipRect.left() - kEpsilon);
REPORTER_ASSERT(reporter, octoBounds.top() >= (float)clipRect.top() - kEpsilon);
REPORTER_ASSERT(reporter, octoBounds.right() <= (float)clipRect.right() + kEpsilon);
REPORTER_ASSERT(reporter, octoBounds.bottom() <= (float)clipRect.bottom() + kEpsilon);
octoBounds.validateBoundsAreTight([reporter](
bool cond, const char* file, int line, const char* code) {
if (!cond) {
reporter->reportFailedWithContext(skiatest::Failure(file, line, code, SkString()));
}
});
}
// This is a variant of SkRandom::nextRangeU that can handle negative numbers. As currently written,
// and assuming two's compliment, it would probably work to just call the existing nextRangeU
// implementation with negative value(s), but we go through this method as an extra precaution.
static int next_range_i(SkRandom* rand, int min, int max) {
int u = rand->nextRangeU(0, max - min);
return u + min;
}
static void test_octagon(Reporter* reporter, SkRandom* rand, float l, float t, float r, float b) {
for (int i = 0; i < 20; ++i) {
float minL45 = GrOctoBounds::Get_x45(l,b);
float maxL45 = std::min(GrOctoBounds::Get_x45(r,b), GrOctoBounds::Get_x45(l,t));
float minT45 = GrOctoBounds::Get_y45(l,t);
float maxT45 = std::min(GrOctoBounds::Get_y45(l,b), GrOctoBounds::Get_y45(r,t));
float minR45 = std::max(GrOctoBounds::Get_x45(l,t), GrOctoBounds::Get_x45(r,b));
float maxR45 = GrOctoBounds::Get_x45(r,t);
float minB45 = std::max(GrOctoBounds::Get_y45(r,t), GrOctoBounds::Get_y45(l,b));
float maxB45 = GrOctoBounds::Get_y45(r,b);
// Pick somewhat valid 45 degree bounds.
float l45 = rand->nextRangeF(minL45, maxL45);
float t45 = rand->nextRangeF(minT45, maxT45);
float r45 = rand->nextRangeF(minR45, maxR45);
float b45 = rand->nextRangeF(minB45, maxB45);
// Grow out diagonal corners if too tight, making 45 bounds valid.
std::function<void()> growOutDiagonals[4] = {
[&]() { // Push top-left diagonal corner outside left edge.
float miss = GrOctoBounds::Get_x(l45,t45) - l;
if (miss > 0) {
// x = (x45 + y45)/2
l45 -= miss;
if (l45 < minL45) {
t45 -= minL45 - l45;
l45 = minL45;
}
t45 -= miss;
if (t45 < minT45) {
l45 -= minT45 - t45;
t45 = minT45;
}
}
},
[&]() { // Push top-right diagonal corner outside top edge.
float miss = GrOctoBounds::Get_y(r45,t45) - t;
if (miss > 0) {
// y = (y45 - x45)/2
r45 += miss;
if (r45 > maxR45) {
t45 -= r45 - maxR45;
r45 = maxR45;
}
t45 -= miss;
if (t45 < minT45) {
r45 += minT45 - t45;
t45 = minT45;
}
}
},
[&]() { // Push bottom-right diagonal corner outside right edge.
float miss = r - GrOctoBounds::Get_x(r45,b45);
if (miss > 0) {
// x = (x45 + y45)/2
r45 += miss;
if (r45 > maxR45) {
b45 += r45 - maxR45;
r45 = maxR45;
}
b45 += miss;
if (b45 > maxB45) {
r45 += b45 - maxB45;
b45 = maxB45;
}
}
},
[&]() { // Push bottom-left diagonal corner outside bottom edge.
float miss = b - GrOctoBounds::Get_y(l45,b45);
if (miss > 0) {
// y = (y45 - x45)/2
l45 -= miss;
if (l45 < minL45) {
b45 += minL45 - l45;
l45 = minL45;
}
b45 += miss;
if (b45 > maxB45) {
l45 -= b45 - maxB45;
b45 = maxB45;
}
}
},
};
// Shuffle.
for (int i = 0; i < 10; ++i) {
std::swap(growOutDiagonals[rand->nextRangeU(0, 3)],
growOutDiagonals[rand->nextRangeU(0, 3)]);
}
for (const auto& f : growOutDiagonals) {
f();
}
GrOctoBounds octoBounds(SkRect::MakeLTRB(l,t,r,b), SkRect::MakeLTRB(l45,t45,r45,b45));
SkIRect devIBounds;
octoBounds.roundOut(&devIBounds);
// Test a clip rect that completely encloses the octagon.
bool clipSuccess = octoBounds.clip(devIBounds);
REPORTER_ASSERT(reporter, clipSuccess);
// Should not have clipped anything.
REPORTER_ASSERT(reporter, octoBounds == GrOctoBounds({l,t,r,b}, {l45,t45,r45,b45}));
validate_octo_bounds(reporter, devIBounds, octoBounds);
// Test a bunch of random clip rects.
for (int j = 0; j < 20; ++j) {
SkIRect clipRect;
do {
clipRect.fLeft = next_range_i(rand, devIBounds.left(), devIBounds.right() - 1);
clipRect.fTop = next_range_i(rand, devIBounds.top(), devIBounds.bottom() - 1);
clipRect.fRight = next_range_i(rand, clipRect.left() + 1, devIBounds.right());
clipRect.fBottom = next_range_i(rand, clipRect.top() + 1, devIBounds.bottom());
} while (clipRect == devIBounds);
GrOctoBounds octoBoundsClipped = octoBounds;
if (!octoBoundsClipped.clip(clipRect)) {
// Ensure clipRect is completely outside one of the diagonals.
float il = (float)clipRect.left();
float it = (float)clipRect.top();
float ir = (float)clipRect.right();
float ib = (float)clipRect.bottom();
REPORTER_ASSERT(reporter,
GrOctoBounds::Get_x45(ir,it) <= l45 + kEpsilon ||
GrOctoBounds::Get_y45(ir,ib) <= t45 + kEpsilon ||
GrOctoBounds::Get_x45(il,ib) >= r45 - kEpsilon ||
GrOctoBounds::Get_y45(il,it) >= b45 - kEpsilon);
++numClipsOut;
} else {
validate_octo_bounds(reporter, clipRect, octoBoundsClipped);
++numIntersectClips;
}
}
}
}
DEF_TEST(OctoBounds, reporter) {
numClipsOut = 0;
numIntersectClips = 0;
SkRandom rand;
test_octagon(reporter, &rand, 0, 0, 100, 100);
test_octagon(reporter, &rand, -2, 0, 2, 100);
test_octagon(reporter, &rand, 0, -10, 100, 0);
// We can't test Infs or NaNs because they trigger internal asserts when setting GrOctoBounds.
// Sanity check on our random clip testing.. Just make we hit both types of clip.
REPORTER_ASSERT(reporter, numClipsOut > 0);
REPORTER_ASSERT(reporter, numIntersectClips > 0);
}