/*
 * Copyright 2019 Google LLC.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "src/gpu/ganesh/ops/PathInnerTriangulateOp.h"

#include "src/gpu/ganesh/GrEagerVertexAllocator.h"
#include "src/gpu/ganesh/GrGpu.h"
#include "src/gpu/ganesh/GrOpFlushState.h"
#include "src/gpu/ganesh/GrRecordingContextPriv.h"
#include "src/gpu/ganesh/GrResourceProvider.h"
#include "src/gpu/ganesh/glsl/GrGLSLVertexGeoBuilder.h"
#include "src/gpu/ganesh/tessellate/GrPathTessellationShader.h"
#include "src/gpu/ganesh/tessellate/PathTessellator.h"

#if !defined(SK_ENABLE_OPTIMIZE_SIZE)

namespace skgpu::ganesh {

namespace {

// Fills an array of convex hulls surrounding 4-point cubic or conic instances. This shader is used
// for the "cover" pass after the curves have been fully stencilled.
class HullShader : public GrPathTessellationShader {
public:
    HullShader(const SkMatrix& viewMatrix, SkPMColor4f color, const GrShaderCaps& shaderCaps)
            : GrPathTessellationShader(kTessellate_HullShader_ClassID,
                                       GrPrimitiveType::kTriangleStrip,
                                       viewMatrix,
                                       color,
                                       PatchAttribs::kNone) {
        fInstanceAttribs.emplace_back("p01", kFloat4_GrVertexAttribType, SkSLType::kFloat4);
        fInstanceAttribs.emplace_back("p23", kFloat4_GrVertexAttribType, SkSLType::kFloat4);
        if (!shaderCaps.fInfinitySupport) {
            // A conic curve is written out with p3=[w,Infinity], but GPUs that don't support
            // infinity can't detect this. On these platforms we also write out an extra float with
            // each patch that explicitly tells the shader what type of curve it is.
            fInstanceAttribs.emplace_back("curveType", kFloat_GrVertexAttribType, SkSLType::kFloat);
        }
        this->setInstanceAttributesWithImplicitOffsets(fInstanceAttribs.data(),
                                                       fInstanceAttribs.size());
        SkASSERT(fInstanceAttribs.size() <= kMaxInstanceAttribCount);

        if (!shaderCaps.fVertexIDSupport) {
            constexpr static Attribute kVertexIdxAttrib("vertexidx", kFloat_GrVertexAttribType,
                                                        SkSLType::kFloat);
            this->setVertexAttributesWithImplicitOffsets(&kVertexIdxAttrib, 1);
        }
    }

private:
    const char* name() const final { return "tessellate_HullShader"; }
    void addToKey(const GrShaderCaps&, KeyBuilder*) const final {}
    std::unique_ptr<ProgramImpl> makeProgramImpl(const GrShaderCaps&) const final;

    constexpr static int kMaxInstanceAttribCount = 3;
    SkSTArray<kMaxInstanceAttribCount, Attribute> fInstanceAttribs;
};

std::unique_ptr<GrGeometryProcessor::ProgramImpl> HullShader::makeProgramImpl(
        const GrShaderCaps&) const {
    class Impl : public GrPathTessellationShader::Impl {
        void emitVertexCode(const GrShaderCaps& shaderCaps,
                            const GrPathTessellationShader&,
                            GrGLSLVertexBuilder* v,
                            GrGLSLVaryingHandler*,
                            GrGPArgs* gpArgs) override {
            if (shaderCaps.fInfinitySupport) {
                v->insertFunction(
                "bool is_conic_curve() { return isinf(p23.w); }"
                "bool is_non_triangular_conic_curve() {"
                    // We consider a conic non-triangular as long as its weight isn't infinity.
                    // NOTE: "isinf == false" works on Mac Radeon GLSL; "!isinf" can get the wrong
                    // answer.
                    "return isinf(p23.z) == false;"
                "}"
                );
            } else {
                v->insertFunction(SkStringPrintf(
                "bool is_conic_curve() { return curveType != %g; }",
                        tess::kCubicCurveType).c_str());
                v->insertFunction(SkStringPrintf(
                "bool is_non_triangular_conic_curve() {"
                    "return curveType == %g;"
                "}", tess::kConicCurveType).c_str());
            }
            v->codeAppend(
            "float2 p0=p01.xy, p1=p01.zw, p2=p23.xy, p3=p23.zw;"
            "if (is_conic_curve()) {"
                // Conics are 3 points, with the weight in p3.
                "float w = p3.x;"
                "p3 = p2;"  // Duplicate the endpoint for shared code that also runs on cubics.
                "if (is_non_triangular_conic_curve()) {"
                    // Convert the points to a trapeziodal hull that circumcscribes the conic.
                    "float2 p1w = p1 * w;"
                    "float T = .51;"  // Bias outward a bit to ensure we cover the outermost samples.
                    "float2 c1 = mix(p0, p1w, T);"
                    "float2 c2 = mix(p2, p1w, T);"
                    "float iw = 1 / mix(1, w, T);"
                    "p2 = c2 * iw;"
                    "p1 = c1 * iw;"
                "}"
            "}"

            // Translate the points to v0..3 where v0=0.
            "float2 v1 = p1 - p0;"
            "float2 v2 = p2 - p0;"
            "float2 v3 = p3 - p0;"

            // Reorder the points so v2 bisects v1 and v3.
            "if (sign(cross_length_2d(v2, v1)) == sign(cross_length_2d(v2, v3))) {"
                "float2 tmp = p2;"
                "if (sign(cross_length_2d(v1, v2)) != sign(cross_length_2d(v1, v3))) {"
                    "p2 = p1;"  // swap(p2, p1)
                    "p1 = tmp;"
                "} else {"
                    "p2 = p3;"  // swap(p2, p3)
                    "p3 = tmp;"
                "}"
            "}"
            );

            if (shaderCaps.fVertexIDSupport) {
                // If we don't have sk_VertexID support then "vertexidx" already came in as a
                // vertex attrib.
                v->codeAppend(
                // sk_VertexID comes in fan order. Convert to strip order.
                "int vertexidx = sk_VertexID;"
                "vertexidx ^= vertexidx >> 1;");
            }

            v->codeAppend(
            // Find the "turn direction" of each corner and net turn direction.
            "float vertexdir = 0;"
            "float netdir = 0;"
            "float2 prev, next;"
            "float dir;"
            "float2 localcoord;"
            "float2 nextcoord;"
            );

            for (int i = 0; i < 4; ++i) {
                v->codeAppendf(
                "prev = p%i - p%i;", i, (i + 3) % 4);
                v->codeAppendf(
                "next = p%i - p%i;", (i + 1) % 4, i);
                v->codeAppendf(
                "dir = sign(cross_length_2d(prev, next));"
                "if (vertexidx == %i) {"
                    "vertexdir = dir;"
                    "localcoord = p%i;"
                    "nextcoord = p%i;"
                "}"
                "netdir += dir;", i, i, (i + 1) % 4);
            }

            v->codeAppend(
            // Remove the non-convex vertex, if any.
            "if (vertexdir != sign(netdir)) {"
                "localcoord = nextcoord;"
            "}"

            "float2 vertexpos = AFFINE_MATRIX * localcoord + TRANSLATE;");
            gpArgs->fLocalCoordVar.set(SkSLType::kFloat2, "localcoord");
            gpArgs->fPositionVar.set(SkSLType::kFloat2, "vertexpos");
        }
    };
    return std::make_unique<Impl>();
}

}  // anonymous namespace

void PathInnerTriangulateOp::visitProxies(const GrVisitProxyFunc& func) const {
    if (fPipelineForFills) {
        fPipelineForFills->visitProxies(func);
    } else {
        fProcessors.visitProxies(func);
    }
}

GrDrawOp::FixedFunctionFlags PathInnerTriangulateOp::fixedFunctionFlags() const {
    auto flags = FixedFunctionFlags::kUsesStencil;
    if (GrAAType::kNone != fAAType) {
        flags |= FixedFunctionFlags::kUsesHWAA;
    }
    return flags;
}

GrProcessorSet::Analysis PathInnerTriangulateOp::finalize(const GrCaps& caps,
                                                          const GrAppliedClip* clip,
                                                          GrClampType clampType) {
    return fProcessors.finalize(fColor, GrProcessorAnalysisCoverage::kNone, clip, nullptr, caps,
                                clampType, &fColor);
}

void PathInnerTriangulateOp::pushFanStencilProgram(const GrTessellationShader::ProgramArgs& args,
                                                   const GrPipeline* pipelineForStencils,
                                                   const GrUserStencilSettings* stencil) {
    SkASSERT(pipelineForStencils);
    auto shader = GrPathTessellationShader::MakeSimpleTriangleShader(args.fArena, fViewMatrix,
                                                                     SK_PMColor4fTRANSPARENT);
    fFanPrograms.push_back(GrTessellationShader::MakeProgram(args, shader, pipelineForStencils,
                                                             stencil)); }

void PathInnerTriangulateOp::pushFanFillProgram(const GrTessellationShader::ProgramArgs& args,
                                                const GrUserStencilSettings* stencil) {
    SkASSERT(fPipelineForFills);
    auto shader = GrPathTessellationShader::MakeSimpleTriangleShader(args.fArena, fViewMatrix,
                                                                     fColor);
    fFanPrograms.push_back(GrTessellationShader::MakeProgram(args, shader, fPipelineForFills,
                                                             stencil));
}

void PathInnerTriangulateOp::prePreparePrograms(const GrTessellationShader::ProgramArgs& args,
                                                GrAppliedClip&& appliedClip) {
    SkASSERT(!fFanTriangulator);
    SkASSERT(!fFanPolys);
    SkASSERT(!fPipelineForFills);
    SkASSERT(!fTessellator);
    SkASSERT(!fStencilCurvesProgram);
    SkASSERT(fFanPrograms.empty());
    SkASSERT(!fCoverHullsProgram);

    if (fPath.countVerbs() <= 0) {
        return;
    }

    // If using wireframe, we have to fall back on a standard Redbook "stencil then cover" algorithm
    // instead of bypassing the stencil buffer to fill the fan directly.
    bool forceRedbookStencilPass =
            (fPathFlags & (FillPathFlags::kStencilOnly | FillPathFlags::kWireframe));
    bool doFill = !(fPathFlags & FillPathFlags::kStencilOnly);

    bool isLinear;
    fFanTriangulator = args.fArena->make<GrInnerFanTriangulator>(fPath, args.fArena);
    fFanPolys = fFanTriangulator->pathToPolys(&fFanBreadcrumbs, &isLinear);

    // Create a pipeline for stencil passes if needed.
    const GrPipeline* pipelineForStencils = nullptr;
    if (forceRedbookStencilPass || !isLinear) {  // Curves always get stencilled.
        auto pipelineFlags = (fPathFlags & FillPathFlags::kWireframe)
                ? GrPipeline::InputFlags::kWireframe
                : GrPipeline::InputFlags::kNone;
        pipelineForStencils = GrPathTessellationShader::MakeStencilOnlyPipeline(
                args, fAAType, appliedClip.hardClip(), pipelineFlags);
    }

    // Create a pipeline for fill passes if needed.
    if (doFill) {
        fPipelineForFills = GrTessellationShader::MakePipeline(args, fAAType,
                                                               std::move(appliedClip),
                                                               std::move(fProcessors));
    }

    // Pass 1: Tessellate the outer curves into the stencil buffer.
    if (!isLinear) {
        fTessellator = PathCurveTessellator::Make(args.fArena,
                                                  args.fCaps->shaderCaps()->fInfinitySupport);
        auto* tessShader = GrPathTessellationShader::Make(*args.fCaps->shaderCaps(),
                                                          args.fArena,
                                                          fViewMatrix,
                                                          SK_PMColor4fTRANSPARENT,
                                                          fTessellator->patchAttribs());
        const GrUserStencilSettings* stencilPathSettings =
                GrPathTessellationShader::StencilPathSettings(GrFillRuleForSkPath(fPath));
        fStencilCurvesProgram = GrTessellationShader::MakeProgram(args,
                                                                  tessShader,
                                                                  pipelineForStencils,
                                                                  stencilPathSettings);
    }

    // Pass 2: Fill the path's inner fan with a stencil test against the curves.
    if (fFanPolys) {
        if (forceRedbookStencilPass) {
            // Use a standard Redbook "stencil then cover" algorithm instead of bypassing the
            // stencil buffer to fill the fan directly.
            const GrUserStencilSettings* stencilPathSettings =
                    GrPathTessellationShader::StencilPathSettings(GrFillRuleForSkPath(fPath));
            this->pushFanStencilProgram(args, pipelineForStencils, stencilPathSettings);
            if (doFill) {
                this->pushFanFillProgram(args,
                                         GrPathTessellationShader::TestAndResetStencilSettings());
            }
        } else if (isLinear) {
            // There are no outer curves! Ignore stencil and fill the path directly.
            SkASSERT(!pipelineForStencils);
            this->pushFanFillProgram(args, &GrUserStencilSettings::kUnused);
        } else if (!fPipelineForFills->hasStencilClip()) {
            // These are a twist on the standard Redbook stencil settings that allow us to fill the
            // inner polygon directly to the final render target. By the time these programs
            // execute, the outer curves will already be stencilled in. So if the stencil value is
            // zero, then it means the sample in question is not affected by any curves and we can
            // fill it in directly. If the stencil value is nonzero, then we don't fill and instead
            // continue the standard Redbook counting process.
            constexpr static GrUserStencilSettings kFillOrIncrDecrStencil(
                GrUserStencilSettings::StaticInitSeparate<
                    0x0000,                       0x0000,
                    GrUserStencilTest::kEqual,    GrUserStencilTest::kEqual,
                    0xffff,                       0xffff,
                    GrUserStencilOp::kKeep,       GrUserStencilOp::kKeep,
                    GrUserStencilOp::kIncWrap,    GrUserStencilOp::kDecWrap,
                    0xffff,                       0xffff>());

            constexpr static GrUserStencilSettings kFillOrInvertStencil(
                GrUserStencilSettings::StaticInit<
                    0x0000,
                    GrUserStencilTest::kEqual,
                    0xffff,
                    GrUserStencilOp::kKeep,
                    // "Zero" instead of "Invert" because the fan only touches any given pixel once.
                    GrUserStencilOp::kZero,
                    0xffff>());

            auto* stencil = (fPath.getFillType() == SkPathFillType::kWinding)
                    ? &kFillOrIncrDecrStencil
                    : &kFillOrInvertStencil;
            this->pushFanFillProgram(args, stencil);
        } else {
            // This is the same idea as above, but we use two passes instead of one because there is
            // a stencil clip. The stencil test isn't expressive enough to do the above tests and
            // also check the clip bit in a single pass.
            constexpr static GrUserStencilSettings kFillIfZeroAndInClip(
                GrUserStencilSettings::StaticInit<
                    0x0000,
                    GrUserStencilTest::kEqualIfInClip,
                    0xffff,
                    GrUserStencilOp::kKeep,
                    GrUserStencilOp::kKeep,
                    0xffff>());

            constexpr static GrUserStencilSettings kIncrDecrStencilIfNonzero(
                GrUserStencilSettings::StaticInitSeparate<
                    0x0000,                         0x0000,
                    // No need to check the clip because the previous stencil pass will have only
                    // written to samples already inside the clip.
                    GrUserStencilTest::kNotEqual,   GrUserStencilTest::kNotEqual,
                    0xffff,                         0xffff,
                    GrUserStencilOp::kIncWrap,      GrUserStencilOp::kDecWrap,
                    GrUserStencilOp::kKeep,         GrUserStencilOp::kKeep,
                    0xffff,                         0xffff>());

            constexpr static GrUserStencilSettings kInvertStencilIfNonZero(
                GrUserStencilSettings::StaticInit<
                    0x0000,
                    // No need to check the clip because the previous stencil pass will have only
                    // written to samples already inside the clip.
                    GrUserStencilTest::kNotEqual,
                    0xffff,
                    // "Zero" instead of "Invert" because the fan only touches any given pixel once.
                    GrUserStencilOp::kZero,
                    GrUserStencilOp::kKeep,
                    0xffff>());

            // Pass 2a: Directly fill fan samples whose stencil values (from curves) are zero.
            this->pushFanFillProgram(args, &kFillIfZeroAndInClip);

            // Pass 2b: Redbook counting on fan samples whose stencil values (from curves) != 0.
            auto* stencil = (fPath.getFillType() == SkPathFillType::kWinding)
                    ? &kIncrDecrStencilIfNonzero
                    : &kInvertStencilIfNonZero;
            this->pushFanStencilProgram(args, pipelineForStencils, stencil);
        }
    }

    // Pass 3: Draw convex hulls around each curve.
    if (doFill && !isLinear) {
        // By the time this program executes, every pixel will be filled in except the ones touched
        // by curves. We issue a final cover pass over the curves by drawing their convex hulls.
        // This will fill in any remaining samples and reset the stencil values back to zero.
        SkASSERT(fTessellator);
        auto* hullShader = args.fArena->make<HullShader>(fViewMatrix, fColor,
                                                         *args.fCaps->shaderCaps());
        fCoverHullsProgram = GrTessellationShader::MakeProgram(
                args, hullShader, fPipelineForFills,
                GrPathTessellationShader::TestAndResetStencilSettings());
    }
}

void PathInnerTriangulateOp::onPrePrepare(GrRecordingContext* context,
                                          const GrSurfaceProxyView& writeView,
                                          GrAppliedClip* clip,
                                          const GrDstProxyView& dstProxyView,
                                          GrXferBarrierFlags renderPassXferBarriers,
                                          GrLoadOp colorLoadOp) {
    // DMSAA is not supported on DDL.
    bool usesMSAASurface = writeView.asRenderTargetProxy()->numSamples() > 1;
    this->prePreparePrograms({context->priv().recordTimeAllocator(), writeView, usesMSAASurface,
                             &dstProxyView, renderPassXferBarriers, colorLoadOp,
                             context->priv().caps()},
                             (clip) ? std::move(*clip) : GrAppliedClip::Disabled());
    if (fStencilCurvesProgram) {
        context->priv().recordProgramInfo(fStencilCurvesProgram);
    }
    for (const GrProgramInfo* fanProgram : fFanPrograms) {
        context->priv().recordProgramInfo(fanProgram);
    }
    if (fCoverHullsProgram) {
        context->priv().recordProgramInfo(fCoverHullsProgram);
    }
}

SKGPU_DECLARE_STATIC_UNIQUE_KEY(gHullVertexBufferKey);

void PathInnerTriangulateOp::onPrepare(GrOpFlushState* flushState) {
    const GrCaps& caps = flushState->caps();

    if (!fFanTriangulator) {
        this->prePreparePrograms({flushState->allocator(), flushState->writeView(),
                                 flushState->usesMSAASurface(), &flushState->dstProxyView(),
                                 flushState->renderPassBarriers(), flushState->colorLoadOp(),
                                 &caps}, flushState->detachAppliedClip());
        if (!fFanTriangulator) {
            return;
        }
    }

    if (fFanPolys) {
        GrEagerDynamicVertexAllocator alloc(flushState, &fFanBuffer, &fBaseFanVertex);
        fFanVertexCount = fFanTriangulator->polysToTriangles(fFanPolys, &alloc, &fFanBreadcrumbs);
    }

    if (fTessellator) {
        auto tessShader = &fStencilCurvesProgram->geomProc().cast<GrPathTessellationShader>();
        fTessellator->prepareWithTriangles(flushState,
                                           tessShader->viewMatrix(),
                                           &fFanBreadcrumbs,
                                           {SkMatrix::I(), fPath, SK_PMColor4fTRANSPARENT},
                                           fPath.countVerbs());
    }

    if (!caps.shaderCaps()->fVertexIDSupport) {
        constexpr static float kStripOrderIDs[4] = {0, 1, 3, 2};

        SKGPU_DEFINE_STATIC_UNIQUE_KEY(gHullVertexBufferKey);

        fHullVertexBufferIfNoIDSupport = flushState->resourceProvider()->findOrMakeStaticBuffer(
                GrGpuBufferType::kVertex, sizeof(kStripOrderIDs), kStripOrderIDs,
                gHullVertexBufferKey);
    }
}

void PathInnerTriangulateOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
    if (fCoverHullsProgram &&
        fCoverHullsProgram->geomProc().hasVertexAttributes() &&
        !fHullVertexBufferIfNoIDSupport) {
        return;
    }

    if (fStencilCurvesProgram) {
        SkASSERT(fTessellator);
        flushState->bindPipelineAndScissorClip(*fStencilCurvesProgram, this->bounds());
        fTessellator->draw(flushState);
    }

    // Allocation of the fan vertex buffer may have failed but we already pushed back fan programs.
    if (fFanBuffer) {
        for (const GrProgramInfo* fanProgram : fFanPrograms) {
            flushState->bindPipelineAndScissorClip(*fanProgram, this->bounds());
            flushState->bindTextures(fanProgram->geomProc(), nullptr, fanProgram->pipeline());
            flushState->bindBuffers(nullptr, nullptr, fFanBuffer);
            flushState->draw(fFanVertexCount, fBaseFanVertex);
        }
    }

    if (fCoverHullsProgram) {
        SkASSERT(fTessellator);
        flushState->bindPipelineAndScissorClip(*fCoverHullsProgram, this->bounds());
        flushState->bindTextures(fCoverHullsProgram->geomProc(), nullptr, *fPipelineForFills);
        fTessellator->drawHullInstances(flushState, fHullVertexBufferIfNoIDSupport);
    }
}

}  // namespace skgpu::ganesh

#endif // SK_ENABLE_OPTIMIZE_SIZE
