Prevent outset Ws from going negative

Bug: skia:9028
Change-Id: I8e3d37050d3fce7602eee62ae911eae756e603a2
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/211100
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
diff --git a/gm/postercircle.cpp b/gm/postercircle.cpp
new file mode 100644
index 0000000..49a06ab
--- /dev/null
+++ b/gm/postercircle.cpp
@@ -0,0 +1,159 @@
+/*
+ * 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 "gm/gm.h"
+#include "include/core/SkCanvas.h"
+#include "include/core/SkFont.h"
+#include "include/core/SkImage.h"
+#include "include/core/SkRRect.h"
+#include "include/core/SkSurface.h"
+#include "tools/timer/AnimTimer.h"
+
+// Mimics https://output.jsbin.com/falefice/1/quiet?CC_POSTER_CIRCLE, which can't be captured as
+// an SKP due to many 3D layers being composited post-SKP capture.
+// See skbug.com/9028
+class PosterCircleGM : public skiagm::GM {
+public:
+    PosterCircleGM() : fTime(0.f) {}
+
+protected:
+
+    SkString onShortName() override {
+        return SkString("poster_circle");
+    }
+
+    SkISize onISize() override {
+        return SkISize::Make(kStageWidth, kStageHeight + 50);
+    }
+
+    bool onAnimate(const AnimTimer& timer) override {
+#if 0
+        if (timer.isRunning()) {
+            // Use a fixed timestep
+            fTime += 5e-4f;
+        }
+#else
+        fTime = timer.scaled(0.5f);
+#endif
+        return true;
+    }
+
+    void onOnceBeforeDraw() override {
+        SkFont font;
+        font.setEdging(SkFont::Edging::kAntiAlias);
+        font.setEmbolden(true);
+        font.setSize(24.f);
+
+        sk_sp<SkSurface> surface = SkSurface::MakeRasterN32Premul(kPosterSize, kPosterSize);
+        for (int i = 0; i < kNumAngles; ++i) {
+            SkCanvas* canvas = surface->getCanvas();
+
+            SkPaint fillPaint;
+            fillPaint.setAntiAlias(true);
+            fillPaint.setColor(i % 2 == 0 ? SkColorSetRGB(0x99, 0x5C, 0x7F)
+                                          : SkColorSetRGB(0x83, 0x5A, 0x99));
+            canvas->drawRRect(SkRRect::MakeRectXY(SkRect::MakeWH(kPosterSize, kPosterSize),
+                                                  10.f, 10.f), fillPaint);
+
+            SkString label;
+            label.printf("%d", i);
+            SkRect labelBounds;
+            font.measureText(label.c_str(), label.size(), kUTF8_SkTextEncoding, &labelBounds);
+            SkScalar labelX = 0.5f * kPosterSize - 0.5f * labelBounds.width();
+            SkScalar labelY = 0.5f * kPosterSize + 0.5f * labelBounds.height();
+
+
+            SkPaint labelPaint;
+            labelPaint.setAntiAlias(true);
+            canvas->drawString(label, labelX, labelY, font, labelPaint);
+
+            fPosterImages[i] = surface->makeImageSnapshot();
+        }
+    }
+
+    void onDraw(SkCanvas* canvas) override {
+        // See https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/perspective
+        // for projection matrix when --webkit-perspective: 800px is used.
+        SkMatrix44 proj(SkMatrix44::kIdentity_Constructor);
+        proj.set(3, 2, -1.f / 800.f);
+
+        for (int pass = 0; pass < 2; ++pass) {
+            // Want to draw 90 to 270 first (the back), then 270 to 90 (the front), but do all 3
+            // rings backsides, then their frontsides since the front projections overlap across
+            // rings. Note: we skip the poster circle's x axis rotation because that complicates the
+            // back-to-front drawing order and it isn't necessary to trigger draws aligned with Z.
+            bool drawFront = pass > 0;
+
+            for (int y = 0; y < 3; ++y) {
+                float ringY = (y - 1) * (kPosterSize + 10.f);
+                for (int i = 0; i < kNumAngles; ++i) {
+                    // Add an extra 45 degree rotation, which triggers the bug by aligning some of
+                    // the posters with the z axis.
+                    SkScalar yDuration = 5.f - y;
+                    SkScalar yRotation = SkScalarMod(kAngleStep * i +
+                            360.f * SkScalarMod(fTime / yDuration, yDuration), 360.f);
+                    // These rotation limits were chosen manually to line up with current projection
+                    static constexpr SkScalar kBackMinAngle = 70.f;
+                    static constexpr SkScalar kBackMaxAngle = 290.f;
+                    if (drawFront) {
+                        if (yRotation >= kBackMinAngle && yRotation <= kBackMaxAngle) {
+                            // Back portion during a front draw
+                            continue;
+                        }
+                    } else {
+                        if (yRotation < kBackMinAngle || yRotation > kBackMaxAngle) {
+                            // Front portion during a back draw
+                            continue;
+                        }
+                    }
+
+                    canvas->save();
+
+                    // Matrix matches transform: rotateY(<angle>deg) translateZ(200px); nested in an
+                    // element with the perspective projection matrix above.
+                    SkMatrix44 model;
+                    // No post/preRotate, so start with rotation matrix and adjust from there
+                    model.setRotateAboutUnit(0.f, 1.f, 0.f, SkDegreesToRadians(yRotation));
+                    model.preTranslate(0.f, 0.f, kRingRadius); // *before* rotation
+                    model.postTranslate(0.f, ringY, 0.f);      // *after* rotation
+                    model.postConcat(proj);
+                    model.postTranslate(0.5f * kStageWidth, 0.5f * kStageHeight + 25, 0.f);
+
+                    // Flatten the 4x4 matrix by discarding the 3rd row and column
+                    canvas->concat(SkMatrix::MakeAll(
+                            model.get(0, 0), model.get(0, 1), model.get(0, 3),
+                            model.get(1, 0), model.get(1, 1), model.get(1, 3),
+                            model.get(3, 0), model.get(3, 1), model.get(3, 3)));
+
+                    SkRect poster = SkRect::MakeLTRB(-0.5f * kPosterSize, -0.5f * kPosterSize,
+                                                      0.5f * kPosterSize,  0.5f * kPosterSize);
+                    SkPaint fillPaint;
+                    fillPaint.setAntiAlias(true);
+                    fillPaint.setAlphaf(0.7f);
+                    fillPaint.setFilterQuality(kLow_SkFilterQuality);
+                    canvas->drawImageRect(fPosterImages[i], poster, &fillPaint);
+
+                    canvas->restore();
+                }
+            }
+        }
+    }
+
+private:
+    static const int kAngleStep = 30;
+    static const int kNumAngles = 12; // 0 through 330 degrees
+
+    static const int kStageWidth = 600;
+    static const int kStageHeight = 400;
+    static const int kRingRadius = 200;
+    static const int kPosterSize = 100;
+
+    sk_sp<SkImage> fPosterImages[kNumAngles];
+    SkScalar fTime;
+};
+
+DEF_GM(return new PosterCircleGM();)
diff --git a/gn/gm.gni b/gn/gm.gni
index 6c97df6..73cb5fa 100644
--- a/gn/gm.gni
+++ b/gn/gm.gni
@@ -275,6 +275,7 @@
   "$_gm/poly2poly.cpp",
   "$_gm/polygons.cpp",
   "$_gm/polygonoffset.cpp",
+  "$_gm/postercircle.cpp",
   "$_gm/quadpaths.cpp",
   "$_gm/radial_gradient_precision.cpp",
   "$_gm/readpixels.cpp",
diff --git a/src/gpu/ops/GrQuadPerEdgeAA.cpp b/src/gpu/ops/GrQuadPerEdgeAA.cpp
index b9a7b45..e2714a6 100644
--- a/src/gpu/ops/GrQuadPerEdgeAA.cpp
+++ b/src/gpu/ops/GrQuadPerEdgeAA.cpp
@@ -258,6 +258,19 @@
                     V4f(0.f)) / denom;                            /* !B      */
     }
 
+    V4f newW = quad->fW + a * e1w + b * e2w;
+    // If newW < 0, scale a and b such that the point reaches the infinity plane instead of crossing
+    // This breaks orthogonality of inset/outsets, but GPUs don't handle negative Ws well so this
+    // is far less visually disturbing (likely not noticeable since it's at extreme perspective).
+    // The alternative correction (multiply xyw by -1) has the disadvantage of changing how local
+    // coordinates would be interpolated.
+    static const float kMinW = 1e-6f;
+    if (any(newW < 0.f)) {
+        V4f scale = if_then_else(newW < kMinW, (kMinW - quad->fW) / (newW - quad->fW), V4f(1.f));
+        a *= scale;
+        b *= scale;
+    }
+
     quad->fX += a * e1x + b * e2x;
     quad->fY += a * e1y + b * e2y;
     quad->fW += a * e1w + b * e2w;