Delete identity matrix shaders from tessellation

Every shader always has a matrix now. The 4 FMAs (literally) that this
saved by specializing the shader weren't worth it.

Bug: skia:10419
Change-Id: I506dbd6d723f6dd022345956fdb5b60b0dd94932
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/412416
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
This commit is contained in:
Chris Dalton 2021-05-25 15:16:29 -06:00 committed by Skia Commit-Bot
parent e1c2beb3be
commit 64d06ba897
7 changed files with 86 additions and 101 deletions

View File

@ -17,14 +17,18 @@ public:
void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override { void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
auto& shader = args.fGeomProc.cast<GrFillPathShader>(); auto& shader = args.fGeomProc.cast<GrFillPathShader>();
const char* viewMatrix;
fViewMatrixUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat3x3_GrSLType, "view_matrix", &viewMatrix);
args.fVaryingHandler->emitAttributes(shader); args.fVaryingHandler->emitAttributes(shader);
const char* affineMatrix, *translate;
fAffineMatrixUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat4_GrSLType, "affineMatrix", &affineMatrix);
fTranslateUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat2_GrSLType, "translate", &translate);
args.fVertBuilder->codeAppendf("float2x2 AFFINE_MATRIX = float2x2(%s);", affineMatrix);
args.fVertBuilder->codeAppendf("float2 TRANSLATE = %s;", translate);
args.fVertBuilder->codeAppend("float2 localcoord, vertexpos;"); args.fVertBuilder->codeAppend("float2 localcoord, vertexpos;");
shader.emitVertexCode(this, args.fVertBuilder, viewMatrix, args.fUniformHandler); shader.emitVertexCode(this, args.fVertBuilder, args.fUniformHandler);
gpArgs->fPositionVar.set(kFloat2_GrSLType, "vertexpos"); gpArgs->fPositionVar.set(kFloat2_GrSLType, "vertexpos");
gpArgs->fLocalCoordVar.set(kFloat2_GrSLType, "localcoord"); gpArgs->fLocalCoordVar.set(kFloat2_GrSLType, "localcoord");
@ -41,13 +45,16 @@ public:
const GrShaderCaps&, const GrShaderCaps&,
const GrGeometryProcessor& geomProc) override { const GrGeometryProcessor& geomProc) override {
const GrFillPathShader& shader = geomProc.cast<GrFillPathShader>(); const GrFillPathShader& shader = geomProc.cast<GrFillPathShader>();
pdman.setSkMatrix(fViewMatrixUniform, shader.viewMatrix()); const SkMatrix& m = shader.viewMatrix();
pdman.set4f(fAffineMatrixUniform, m.getScaleX(), m.getSkewY(), m.getSkewX(), m.getScaleY());
pdman.set2f(fTranslateUniform, m.getTranslateX(), m.getTranslateY());
const SkPMColor4f& color = shader.fColor; const SkPMColor4f& color = shader.fColor;
pdman.set4f(fColorUniform, color.fR, color.fG, color.fB, color.fA); pdman.set4f(fColorUniform, color.fR, color.fG, color.fB, color.fA);
} }
GrGLSLUniformHandler::UniformHandle fViewMatrixUniform; GrGLSLUniformHandler::UniformHandle fAffineMatrixUniform;
GrGLSLUniformHandler::UniformHandle fTranslateUniform;
GrGLSLUniformHandler::UniformHandle fColorUniform; GrGLSLUniformHandler::UniformHandle fColorUniform;
}; };
@ -55,14 +62,14 @@ GrGLSLGeometryProcessor* GrFillPathShader::createGLSLInstance(const GrShaderCaps
return new Impl; return new Impl;
} }
void GrFillTriangleShader::emitVertexCode(Impl*, GrGLSLVertexBuilder* v, const char* viewMatrix, void GrFillTriangleShader::emitVertexCode(Impl*, GrGLSLVertexBuilder* v,
GrGLSLUniformHandler* uniformHandler) const { GrGLSLUniformHandler* uniformHandler) const {
v->codeAppendf(R"( v->codeAppend(R"(
localcoord = input_point; localcoord = input_point;
vertexpos = (%s * float3(localcoord, 1)).xy;)", viewMatrix); vertexpos = AFFINE_MATRIX * localcoord + TRANSLATE;)");
} }
void GrFillCubicHullShader::emitVertexCode(Impl*, GrGLSLVertexBuilder* v, const char* viewMatrix, void GrFillCubicHullShader::emitVertexCode(Impl*, GrGLSLVertexBuilder* v,
GrGLSLUniformHandler* uniformHandler) const { GrGLSLUniformHandler* uniformHandler) const {
v->codeAppend(R"( v->codeAppend(R"(
float4x2 P = float4x2(input_points_0_1, input_points_2_3); float4x2 P = float4x2(input_points_0_1, input_points_2_3);
@ -118,21 +125,19 @@ void GrFillCubicHullShader::emitVertexCode(Impl*, GrGLSLVertexBuilder* v, const
vertexidx = (vertexidx + 1) & 3; vertexidx = (vertexidx + 1) & 3;
} }
localcoord = P[vertexidx];)"); localcoord = P[vertexidx];
vertexpos = AFFINE_MATRIX * localcoord + TRANSLATE;)");
v->codeAppendf("vertexpos = (%s * float3(localcoord, 1)).xy;", viewMatrix);
} }
void GrFillBoundingBoxShader::emitVertexCode(Impl* impl, GrGLSLVertexBuilder* v, void GrFillBoundingBoxShader::emitVertexCode(Impl* impl, GrGLSLVertexBuilder* v,
const char* viewMatrix,
GrGLSLUniformHandler* uniformHandler) const { GrGLSLUniformHandler* uniformHandler) const {
v->codeAppendf(R"( v->codeAppendf(R"(
// Bloat the bounding box by 1/4px to avoid potential T-junctions at the edges. // Bloat the bounding box by 1/4px to avoid potential T-junctions at the edges.
float2x2 M_ = inverse(float2x2(%s)); float2x2 M_ = inverse(AFFINE_MATRIX);
float2 bloat = float2(abs(M_[0]) + abs(M_[1])) * .25; float2 bloat = float2(abs(M_[0]) + abs(M_[1])) * .25;
// Find the vertex position. // Find the vertex position.
float2 T = float2(sk_VertexID & 1, sk_VertexID >> 1); float2 T = float2(sk_VertexID & 1, sk_VertexID >> 1);
localcoord = mix(pathBounds.xy - bloat, pathBounds.zw + bloat, T); localcoord = mix(pathBounds.xy - bloat, pathBounds.zw + bloat, T);
vertexpos = (%s * float3(localcoord, 1)).xy;)", viewMatrix, viewMatrix); vertexpos = AFFINE_MATRIX * localcoord + TRANSLATE;)");
} }

View File

@ -56,8 +56,7 @@ public:
protected: protected:
class Impl; class Impl;
virtual void emitVertexCode(Impl*, GrGLSLVertexBuilder*, const char* viewMatrix, virtual void emitVertexCode(Impl*, GrGLSLVertexBuilder*, GrGLSLUniformHandler*) const = 0;
GrGLSLUniformHandler*) const = 0;
private: private:
const SkPMColor4f fColor; const SkPMColor4f fColor;
@ -76,8 +75,7 @@ public:
private: private:
const char* name() const override { return "GrFillTriangleShader"; } const char* name() const override { return "GrFillTriangleShader"; }
void emitVertexCode(Impl*, GrGLSLVertexBuilder*, const char* viewMatrix, void emitVertexCode(Impl*, GrGLSLVertexBuilder*, GrGLSLUniformHandler*) const override;
GrGLSLUniformHandler*) const override;
}; };
// Fills an array of convex hulls surrounding 4-point cubic instances. // Fills an array of convex hulls surrounding 4-point cubic instances.
@ -94,8 +92,7 @@ public:
private: private:
const char* name() const override { return "GrFillCubicHullShader"; } const char* name() const override { return "GrFillCubicHullShader"; }
void emitVertexCode(Impl*, GrGLSLVertexBuilder*, const char* viewMatrix, void emitVertexCode(Impl*, GrGLSLVertexBuilder*, GrGLSLUniformHandler*) const override;
GrGLSLUniformHandler*) const override;
}; };
// Fills a path's bounding box, with subpixel outset to avoid possible T-junctions with extreme // Fills a path's bounding box, with subpixel outset to avoid possible T-junctions with extreme
@ -114,8 +111,7 @@ public:
private: private:
const char* name() const override { return "GrFillBoundingBoxShader"; } const char* name() const override { return "GrFillBoundingBoxShader"; }
void emitVertexCode(Impl*, GrGLSLVertexBuilder*, const char* viewMatrix, void emitVertexCode(Impl*, GrGLSLVertexBuilder*, GrGLSLUniformHandler*) const override;
GrGLSLUniformHandler*) const override;
}; };
#endif #endif

View File

@ -57,24 +57,23 @@ protected:
args.fVaryingHandler->emitAttributes(shader); args.fVaryingHandler->emitAttributes(shader);
auto v = args.fVertBuilder; auto v = args.fVertBuilder;
GrShaderVar vertexPos = (*shader.vertexAttributes().begin()).asShaderVar(); const char* affineMatrix, *translate;
if (!shader.viewMatrix().isIdentity()) { fAffineMatrixUniform = args.fUniformHandler->addUniform(
const char* viewMatrix; nullptr, kVertex_GrShaderFlag, kFloat4_GrSLType, "affineMatrix", &affineMatrix);
fViewMatrixUniform = args.fUniformHandler->addUniform( fTranslateUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat3x3_GrSLType, "view_matrix", &viewMatrix); nullptr, kVertex_GrShaderFlag, kFloat2_GrSLType, "translate", &translate);
v->codeAppendf("float2 vertexpos = (%s * float3(inputPoint, 1)).xy;", viewMatrix); v->codeAppendf("float2 vertexpos = float2x2(%s) * inputPoint + %s;",
affineMatrix, translate);
if (shader.willUseTessellationShaders()) { if (shader.willUseTessellationShaders()) {
// If y is infinity then x is a conic weight. Don't transform. // If y is infinity then x is a conic weight. Don't transform.
v->codeAppendf("vertexpos = (isinf(inputPoint.y)) ? inputPoint : vertexpos;"); v->codeAppendf("vertexpos = (isinf(inputPoint.y)) ? inputPoint : vertexpos;");
} }
vertexPos.set(kFloat2_GrSLType, "vertexpos");
}
if (!shader.willUseTessellationShaders()) { // This is the case for the triangle shader. if (!shader.willUseTessellationShaders()) { // This is the case for the triangle shader.
gpArgs->fPositionVar = vertexPos; gpArgs->fPositionVar.set(kFloat2_GrSLType, "vertexpos");
} else { } else {
v->declareGlobal(GrShaderVar("P", kFloat2_GrSLType, GrShaderVar::TypeModifier::Out)); v->declareGlobal(GrShaderVar("P", kFloat2_GrSLType, GrShaderVar::TypeModifier::Out));
v->codeAppendf("P = %s;", vertexPos.c_str()); v->codeAppendf("P = %s;", "vertexpos");
} }
// The fragment shader is normally disabled, but output fully opaque white. // The fragment shader is normally disabled, but output fully opaque white.
@ -85,13 +84,13 @@ protected:
void setData(const GrGLSLProgramDataManager& pdman, void setData(const GrGLSLProgramDataManager& pdman,
const GrShaderCaps&, const GrShaderCaps&,
const GrGeometryProcessor& geomProc) override { const GrGeometryProcessor& geomProc) override {
const auto& shader = geomProc.cast<GrStencilPathShader>(); const SkMatrix& m = geomProc.cast<GrStencilPathShader>().viewMatrix();
if (!shader.viewMatrix().isIdentity()) { pdman.set4f(fAffineMatrixUniform, m.getScaleX(), m.getSkewY(), m.getSkewX(), m.getScaleY());
pdman.setSkMatrix(fViewMatrixUniform, shader.viewMatrix()); pdman.set2f(fTranslateUniform, m.getTranslateX(), m.getTranslateY());
}
} }
GrGLSLUniformHandler::UniformHandle fViewMatrixUniform; GrGLSLUniformHandler::UniformHandle fAffineMatrixUniform;
GrGLSLUniformHandler::UniformHandle fTranslateUniform;
}; };
GrGLSLGeometryProcessor* GrStencilPathShader::createGLSLInstance(const GrShaderCaps&) const { GrGLSLGeometryProcessor* GrStencilPathShader::createGLSLInstance(const GrShaderCaps&) const {
@ -375,13 +374,13 @@ class GrCurveMiddleOutShader::Impl : public GrStencilPathShader::Impl {
float T = find_middle_out_T(); float T = find_middle_out_T();
pos = eval_rational_cubic(P, T); pos = eval_rational_cubic(P, T);
})"); })");
if (!shader.viewMatrix().isIdentity()) { const char* affineMatrix, *translate;
const char* viewMatrix; fAffineMatrixUniform = args.fUniformHandler->addUniform(
fViewMatrixUniform = args.fUniformHandler->addUniform( nullptr, kVertex_GrShaderFlag, kFloat4_GrSLType, "affineMatrix", &affineMatrix);
nullptr, kVertex_GrShaderFlag, kFloat3x3_GrSLType, "view_matrix", &viewMatrix); fTranslateUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat2_GrSLType, "translate", &translate);
args.fVertBuilder->codeAppendf(R"( args.fVertBuilder->codeAppendf(R"(
pos = (%s * float3(pos, 1)).xy;)", viewMatrix); pos = float2x2(%s) * pos + %s;)", affineMatrix, translate);
}
gpArgs->fPositionVar.set(kFloat2_GrSLType, "pos"); gpArgs->fPositionVar.set(kFloat2_GrSLType, "pos");
// The fragment shader is normally disabled, but output fully opaque white. // The fragment shader is normally disabled, but output fully opaque white.

View File

@ -85,9 +85,7 @@ public:
protected: protected:
constexpr static Attribute kSinglePointAttrib{"inputPoint", kFloat2_GrVertexAttribType, constexpr static Attribute kSinglePointAttrib{"inputPoint", kFloat2_GrVertexAttribType,
kFloat2_GrSLType}; kFloat2_GrSLType};
void getGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const override { void getGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const override {}
b->add32(this->viewMatrix().isIdentity());
}
GrGLSLGeometryProcessor* createGLSLInstance(const GrShaderCaps&) const override; GrGLSLGeometryProcessor* createGLSLInstance(const GrShaderCaps&) const override;
class Impl; class Impl;

View File

@ -79,17 +79,15 @@ void GrStrokeInstancedShaderImpl::onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) {
} }
// View matrix uniforms. // View matrix uniforms.
if (!shader.viewMatrix().isIdentity()) {
const char* translateName, *affineMatrixName; const char* translateName, *affineMatrixName;
fAffineMatrixUniform = args.fUniformHandler->addUniform( fAffineMatrixUniform = args.fUniformHandler->addUniform(nullptr, kVertex_GrShaderFlag,
nullptr, kVertex_GrShaderFlag, kFloat4_GrSLType, "affineMatrix", kFloat4_GrSLType, "affineMatrix",
&affineMatrixName); &affineMatrixName);
fTranslateUniform = args.fUniformHandler->addUniform( fTranslateUniform = args.fUniformHandler->addUniform(nullptr, kVertex_GrShaderFlag,
nullptr, kVertex_GrShaderFlag, kFloat2_GrSLType, "translate", &translateName); kFloat2_GrSLType, "translate",
args.fVertBuilder->codeAppendf("float2x2 AFFINE_MATRIX = float2x2(%s);\n", &translateName);
affineMatrixName); args.fVertBuilder->codeAppendf("float2x2 AFFINE_MATRIX = float2x2(%s);\n", affineMatrixName);
args.fVertBuilder->codeAppendf("float2 TRANSLATE = %s;\n", translateName); args.fVertBuilder->codeAppendf("float2 TRANSLATE = %s;\n", translateName);
}
// Tessellation code. // Tessellation code.
args.fVertBuilder->codeAppend(R"( args.fVertBuilder->codeAppend(R"(
@ -103,7 +101,7 @@ void GrStrokeInstancedShaderImpl::onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) {
P[3] = P[2]; // Setting p3 equal to p2 works for the remaining rotational logic. P[3] = P[2]; // Setting p3 equal to p2 works for the remaining rotational logic.
})"); })");
} }
if (shader.stroke().isHairlineStyle() && !shader.viewMatrix().isIdentity()) { if (shader.stroke().isHairlineStyle()) {
// Hairline case. Transform the points before tessellation. We can still hold off on the // Hairline case. Transform the points before tessellation. We can still hold off on the
// translate until the end; we just need to perform the scale and skew right now. // translate until the end; we just need to perform the scale and skew right now.
args.fVertBuilder->codeAppend(R"( args.fVertBuilder->codeAppend(R"(

View File

@ -251,11 +251,7 @@ void GrStrokeShaderImpl::emitTessellationCode(const GrStrokeShader& shader, SkSt
float2 ortho = normalize(float2(tangent.y, -tangent.x)); float2 ortho = normalize(float2(tangent.y, -tangent.x));
strokeCoord += ortho * (STROKE_RADIUS * strokeOutset);)"); strokeCoord += ortho * (STROKE_RADIUS * strokeOutset);)");
if (shader.viewMatrix().isIdentity()) { if (!shader.stroke().isHairlineStyle()) {
// No transform matrix.
gpArgs->fPositionVar.set(kFloat2_GrSLType, "strokeCoord");
gpArgs->fLocalCoordVar.set(kFloat2_GrSLType, "strokeCoord");
} else if (!shader.stroke().isHairlineStyle()) {
// Normal case. Do the transform after tessellation. // Normal case. Do the transform after tessellation.
code->append(R"( code->append(R"(
float2 devCoord = AFFINE_MATRIX * strokeCoord + TRANSLATE;)"); float2 devCoord = AFFINE_MATRIX * strokeCoord + TRANSLATE;)");
@ -322,11 +318,9 @@ void GrStrokeShaderImpl::setData(const GrGLSLProgramDataManager& pdman, const Gr
// Set up the view matrix, if any. // Set up the view matrix, if any.
const SkMatrix& m = shader.viewMatrix(); const SkMatrix& m = shader.viewMatrix();
if (!m.isIdentity()) {
pdman.set2f(fTranslateUniform, m.getTranslateX(), m.getTranslateY()); pdman.set2f(fTranslateUniform, m.getTranslateX(), m.getTranslateY());
pdman.set4f(fAffineMatrixUniform, m.getScaleX(), m.getSkewY(), m.getSkewX(), pdman.set4f(fAffineMatrixUniform, m.getScaleX(), m.getSkewY(), m.getSkewX(),
m.getScaleY()); m.getScaleY());
}
if (!shader.hasDynamicColor()) { if (!shader.hasDynamicColor()) {
pdman.set4fv(fColorUniform, 1, shader.color().vec()); pdman.set4fv(fColorUniform, 1, shader.color().vec());
@ -344,7 +338,6 @@ void GrStrokeShader::getGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuil
key = (key << 2) | (uint32_t)fMode; key = (key << 2) | (uint32_t)fMode;
key = (key << 2) | ((keyNeedsJoin) ? fStroke.getJoin() : 0); key = (key << 2) | ((keyNeedsJoin) ? fStroke.getJoin() : 0);
key = (key << 1) | (uint32_t)fStroke.isHairlineStyle(); key = (key << 1) | (uint32_t)fStroke.isHairlineStyle();
key = (key << 1) | (uint32_t)this->viewMatrix().isIdentity();
b->add32(key); b->add32(key);
} }

View File

@ -87,9 +87,9 @@ void GrStrokeTessellationShaderImpl::onEmitCode(EmitArgs& args, GrGPArgs* gpArgs
float JOIN_TYPE = dynamicStrokeAttr.y;)", parametricPrecisionName); float JOIN_TYPE = dynamicStrokeAttr.y;)", parametricPrecisionName);
} }
if (!shader.viewMatrix().isIdentity()) {
fTranslateUniform = uniHandler->addUniform(nullptr, kTessEvaluation_GrShaderFlag, fTranslateUniform = uniHandler->addUniform(nullptr, kTessEvaluation_GrShaderFlag,
kFloat2_GrSLType, "translate", nullptr); kFloat2_GrSLType, "translate", nullptr);
// View matrix uniforms.
const char* affineMatrixName; const char* affineMatrixName;
// Hairlines apply the affine matrix in their vertex shader, prior to tessellation. // Hairlines apply the affine matrix in their vertex shader, prior to tessellation.
// Otherwise the entire view matrix gets applied at the end of the tess eval shader. // Otherwise the entire view matrix gets applied at the end of the tess eval shader.
@ -97,20 +97,18 @@ void GrStrokeTessellationShaderImpl::onEmitCode(EmitArgs& args, GrGPArgs* gpArgs
if (shader.stroke().isHairlineStyle()) { if (shader.stroke().isHairlineStyle()) {
affineMatrixVisibility |= kVertex_GrShaderFlag; affineMatrixVisibility |= kVertex_GrShaderFlag;
} }
fAffineMatrixUniform = uniHandler->addUniform(nullptr, affineMatrixVisibility, fAffineMatrixUniform = uniHandler->addUniform(nullptr, affineMatrixVisibility, kFloat4_GrSLType,
kFloat4_GrSLType, "affineMatrix", "affineMatrix", &affineMatrixName);
&affineMatrixName);
if (affineMatrixVisibility & kVertex_GrShaderFlag) { if (affineMatrixVisibility & kVertex_GrShaderFlag) {
v->codeAppendf("float2x2 AFFINE_MATRIX = float2x2(%s);\n", affineMatrixName); v->codeAppendf("float2x2 AFFINE_MATRIX = float2x2(%s);\n", affineMatrixName);
} }
}
v->codeAppend(R"( v->codeAppend(R"(
// Unpack the control points. // Unpack the control points.
float2 prevControlPoint = prevCtrlPtAttr; float2 prevControlPoint = prevCtrlPtAttr;
float4x2 P = float4x2(pts01Attr, pts23Attr);)"); float4x2 P = float4x2(pts01Attr, pts23Attr);)");
if (shader.stroke().isHairlineStyle() && !shader.viewMatrix().isIdentity()) { if (shader.stroke().isHairlineStyle()) {
// Hairline case. Transform the points before tessellation. We can still hold off on the // Hairline case. Transform the points before tessellation. We can still hold off on the
// translate until the end; we just need to perform the scale and skew right now. // translate until the end; we just need to perform the scale and skew right now.
if (shader.hasConics()) { if (shader.hasConics()) {
@ -558,14 +556,12 @@ SkString GrStrokeTessellationShaderImpl::getTessEvaluationShaderGLSL(
code.appendf("#define STROKE_RADIUS tcsStrokeRadius\n"); code.appendf("#define STROKE_RADIUS tcsStrokeRadius\n");
} }
if (!shader.viewMatrix().isIdentity()) {
const char* translateName = uniformHandler.getUniformCStr(fTranslateUniform); const char* translateName = uniformHandler.getUniformCStr(fTranslateUniform);
code.appendf("uniform vec2 %s;\n", translateName); code.appendf("uniform vec2 %s;\n", translateName);
code.appendf("#define TRANSLATE %s\n", translateName); code.appendf("#define TRANSLATE %s\n", translateName);
const char* affineMatrixName = uniformHandler.getUniformCStr(fAffineMatrixUniform); const char* affineMatrixName = uniformHandler.getUniformCStr(fAffineMatrixUniform);
code.appendf("uniform vec4 %s;\n", affineMatrixName); code.appendf("uniform vec4 %s;\n", affineMatrixName);
code.appendf("#define AFFINE_MATRIX mat2(%s)\n", affineMatrixName); code.appendf("#define AFFINE_MATRIX mat2(%s)\n", affineMatrixName);
}
code.append(R"( code.append(R"(
in vec4 tcsPts01[]; in vec4 tcsPts01[];