in src/gpu/ganesh/effects/GrTextureEffect.cpp [372:735]
void GrTextureEffect::Impl::emitCode(EmitArgs& args) {
using ShaderMode = GrTextureEffect::ShaderMode;
auto& te = args.fFp.cast<GrTextureEffect>();
auto* fb = args.fFragBuilder;
if (te.fShaderModes[0] == ShaderMode::kNone &&
te.fShaderModes[1] == ShaderMode::kNone) {
fb->codeAppendf("return ");
fb->appendTextureLookup(fSamplerHandle, args.fSampleCoord);
fb->codeAppendf(";");
} else {
// Here is the basic flow of the various ShaderModes are implemented in a series of
// steps. Not all the steps apply to all the modes. We try to emit only the steps
// that are necessary for the given x/y shader modes.
//
// 0) Start with interpolated coordinates (unnormalize if doing anything
// complicated).
// 1) Map the coordinates into the subset range [Repeat and MirrorRepeat], or pass
// through output of 0).
// 2) Clamp the coordinates to a 0.5 inset of the subset rect [Clamp, Repeat, and
// MirrorRepeat always or ClampToBorder only when filtering] or pass through
// output of 1). The clamp rect collapses to a line or point it if the subset
// rect is less than one pixel wide/tall.
// 3) Look up texture with output of 2) [All]
// 3) Use the difference between 1) and 2) to apply filtering at edge [Repeat or
// ClampToBorder]. In the Repeat case this requires extra texture lookups on the
// other side of the subset (up to 3 more reads). Or if ClampToBorder and not
// filtering do a hard less than/greater than test with the subset rect.
// Convert possible projective texture coordinates into non-homogeneous half2.
fb->codeAppendf("float2 inCoord = %s;", args.fSampleCoord);
const auto& m = te.fShaderModes;
const char* borderName = nullptr;
if (te.hasClampToBorderShaderMode()) {
fBorderUni = args.fUniformHandler->addUniform(
&te, kFragment_GrShaderFlag, SkSLType::kHalf4, "border", &borderName);
}
auto modeUsesSubset = [](ShaderMode m) {
switch (m) {
case ShaderMode::kNone: return false;
case ShaderMode::kClamp: return false;
case ShaderMode::kRepeat_Nearest_None: return true;
case ShaderMode::kRepeat_Linear_None: return true;
case ShaderMode::kRepeat_Nearest_Mipmap: return true;
case ShaderMode::kRepeat_Linear_Mipmap: return true;
case ShaderMode::kMirrorRepeat: return true;
case ShaderMode::kClampToBorder_Nearest: return true;
case ShaderMode::kClampToBorder_Filter: return true;
}
SkUNREACHABLE;
};
auto modeUsesClamp = [](ShaderMode m) {
switch (m) {
case ShaderMode::kNone: return false;
case ShaderMode::kClamp: return true;
case ShaderMode::kRepeat_Nearest_None: return true;
case ShaderMode::kRepeat_Linear_None: return true;
case ShaderMode::kRepeat_Nearest_Mipmap: return true;
case ShaderMode::kRepeat_Linear_Mipmap: return true;
case ShaderMode::kMirrorRepeat: return true;
case ShaderMode::kClampToBorder_Nearest: return false;
case ShaderMode::kClampToBorder_Filter: return true;
}
SkUNREACHABLE;
};
bool useSubset[2] = {modeUsesSubset(m[0]), modeUsesSubset(m[1])};
bool useClamp [2] = {modeUsesClamp (m[0]), modeUsesClamp (m[1])};
const char* subsetName = nullptr;
if (useSubset[0] || useSubset[1]) {
fSubsetUni = args.fUniformHandler->addUniform(
&te, kFragment_GrShaderFlag, SkSLType::kFloat4, "subset", &subsetName);
}
const char* clampName = nullptr;
if (useClamp[0] || useClamp[1]) {
fClampUni = args.fUniformHandler->addUniform(
&te, kFragment_GrShaderFlag, SkSLType::kFloat4, "clamp", &clampName);
}
bool unormCoordsRequiredForShaderMode = ShaderModeRequiresUnormCoord(m[0]) ||
ShaderModeRequiresUnormCoord(m[1]);
// We should not pre-normalize the input coords with GrMatrixEffect if we're going to
// operate on unnormalized coords and then normalize after the shader mode.
SkASSERT(!(unormCoordsRequiredForShaderMode && te.matrixEffectShouldNormalize()));
bool sampleCoordsMustBeNormalized =
te.fView.asTextureProxy()->textureType() != GrTextureType::kRectangle;
const char* idims = nullptr;
if (unormCoordsRequiredForShaderMode && sampleCoordsMustBeNormalized) {
// TODO: Detect support for textureSize() or polyfill textureSize() in SkSL and
// always use?
fIDimsUni = args.fUniformHandler->addUniform(&te, kFragment_GrShaderFlag,
SkSLType::kFloat2, "idims", &idims);
}
// Generates a string to read at a coordinate, normalizing coords if necessary.
auto read = [&](const char* coord) {
SkString result;
SkString normCoord;
if (idims) {
normCoord.printf("(%s) * %s", coord, idims);
} else {
normCoord = coord;
}
fb->appendTextureLookup(&result, fSamplerHandle, normCoord.c_str());
return result;
};
// Implements coord wrapping for kRepeat and kMirrorRepeat
auto subsetCoord = [&](ShaderMode mode,
const char* coordSwizzle,
const char* subsetStartSwizzle,
const char* subsetStopSwizzle,
const char* extraCoord,
const char* coordWeight) {
switch (mode) {
// These modes either don't use the subset rect or don't need to map the
// coords to be within the subset.
case ShaderMode::kNone:
case ShaderMode::kClampToBorder_Nearest:
case ShaderMode::kClampToBorder_Filter:
case ShaderMode::kClamp:
fb->codeAppendf("subsetCoord.%s = inCoord.%s;", coordSwizzle, coordSwizzle);
break;
case ShaderMode::kRepeat_Nearest_None:
case ShaderMode::kRepeat_Linear_None:
fb->codeAppendf(
"subsetCoord.%s = mod(inCoord.%s - %s.%s, %s.%s - %s.%s) + %s.%s;",
coordSwizzle, coordSwizzle, subsetName, subsetStartSwizzle, subsetName,
subsetStopSwizzle, subsetName, subsetStartSwizzle, subsetName,
subsetStartSwizzle);
break;
case ShaderMode::kRepeat_Nearest_Mipmap:
case ShaderMode::kRepeat_Linear_Mipmap:
// The approach here is to generate two sets of texture coords that
// are both "moving" at the same speed (if not direction) as
// inCoords. We accomplish that by using two out of phase mirror
// repeat coords. We will always sample using both coords but the
// read from the upward sloping one is selected using a weight
// that transitions from one set to the other near the reflection
// point. Like the coords, the weight is a saw-tooth function,
// phase-shifted, vertically translated, and then clamped to 0..1.
// TODO: Skip this and use textureGrad() when available.
SkASSERT(extraCoord);
SkASSERT(coordWeight);
fb->codeAppend("{");
fb->codeAppendf("float w = %s.%s - %s.%s;", subsetName, subsetStopSwizzle,
subsetName, subsetStartSwizzle);
fb->codeAppendf("float w2 = 2 * w;");
fb->codeAppendf("float d = inCoord.%s - %s.%s;", coordSwizzle, subsetName,
subsetStartSwizzle);
fb->codeAppend("float m = mod(d, w2);");
fb->codeAppend("float o = mix(m, w2 - m, step(w, m));");
fb->codeAppendf("subsetCoord.%s = o + %s.%s;", coordSwizzle, subsetName,
subsetStartSwizzle);
fb->codeAppendf("%s = w - o + %s.%s;", extraCoord, subsetName,
subsetStartSwizzle);
// coordWeight is used as the third param of mix() to blend between a
// sample taken using subsetCoord and a sample at extraCoord.
fb->codeAppend("float hw = w/2;");
fb->codeAppend("float n = mod(d - hw, w2);");
fb->codeAppendf("%s = saturate(half(mix(n, w2 - n, step(w, n)) - hw + 0.5));",
coordWeight);
fb->codeAppend("}");
break;
case ShaderMode::kMirrorRepeat:
fb->codeAppend("{");
fb->codeAppendf("float w = %s.%s - %s.%s;", subsetName, subsetStopSwizzle,
subsetName, subsetStartSwizzle);
fb->codeAppendf("float w2 = 2 * w;");
fb->codeAppendf("float m = mod(inCoord.%s - %s.%s, w2);", coordSwizzle,
subsetName, subsetStartSwizzle);
fb->codeAppendf("subsetCoord.%s = mix(m, w2 - m, step(w, m)) + %s.%s;",
coordSwizzle, subsetName, subsetStartSwizzle);
fb->codeAppend("}");
break;
}
};
auto clampCoord = [&](bool clamp,
const char* coordSwizzle,
const char* clampStartSwizzle,
const char* clampStopSwizzle) {
if (clamp) {
fb->codeAppendf("clampedCoord%s = clamp(subsetCoord%s, %s%s, %s%s);",
coordSwizzle, coordSwizzle,
clampName, clampStartSwizzle,
clampName, clampStopSwizzle);
} else {
fb->codeAppendf("clampedCoord%s = subsetCoord%s;", coordSwizzle, coordSwizzle);
}
};
// Insert vars for extra coords and blending weights for repeat + mip map.
const char* extraRepeatCoordX = nullptr;
const char* repeatCoordWeightX = nullptr;
const char* extraRepeatCoordY = nullptr;
const char* repeatCoordWeightY = nullptr;
bool mipmapRepeatX = m[0] == ShaderMode::kRepeat_Nearest_Mipmap ||
m[0] == ShaderMode::kRepeat_Linear_Mipmap;
bool mipmapRepeatY = m[1] == ShaderMode::kRepeat_Nearest_Mipmap ||
m[1] == ShaderMode::kRepeat_Linear_Mipmap;
if (mipmapRepeatX || mipmapRepeatY) {
fb->codeAppend("float2 extraRepeatCoord;");
}
if (mipmapRepeatX) {
fb->codeAppend("half repeatCoordWeightX;");
extraRepeatCoordX = "extraRepeatCoord.x";
repeatCoordWeightX = "repeatCoordWeightX";
}
if (mipmapRepeatY) {
fb->codeAppend("half repeatCoordWeightY;");
extraRepeatCoordY = "extraRepeatCoord.y";
repeatCoordWeightY = "repeatCoordWeightY";
}
// Apply subset rect and clamp rect to coords.
fb->codeAppend("float2 subsetCoord;");
subsetCoord(te.fShaderModes[0], "x", "x", "z", extraRepeatCoordX, repeatCoordWeightX);
subsetCoord(te.fShaderModes[1], "y", "y", "w", extraRepeatCoordY, repeatCoordWeightY);
fb->codeAppend("float2 clampedCoord;");
if (useClamp[0] == useClamp[1]) {
clampCoord(useClamp[0], "", ".xy", ".zw");
} else {
clampCoord(useClamp[0], ".x", ".x", ".z");
clampCoord(useClamp[1], ".y", ".y", ".w");
}
// Additional clamping for the extra coords for kRepeat with mip maps.
if (mipmapRepeatX && mipmapRepeatY) {
fb->codeAppendf("extraRepeatCoord = clamp(extraRepeatCoord, %s.xy, %s.zw);",
clampName, clampName);
} else if (mipmapRepeatX) {
fb->codeAppendf("extraRepeatCoord.x = clamp(extraRepeatCoord.x, %s.x, %s.z);",
clampName, clampName);
} else if (mipmapRepeatY) {
fb->codeAppendf("extraRepeatCoord.y = clamp(extraRepeatCoord.y, %s.y, %s.w);",
clampName, clampName);
}
// Do the 2 or 4 texture reads for kRepeatMipMap and then apply the weight(s)
// to blend between them. If neither direction is repeat or not using mip maps do a single
// read at clampedCoord.
if (mipmapRepeatX && mipmapRepeatY) {
fb->codeAppendf(
"half4 textureColor ="
" mix(mix(%s, %s, repeatCoordWeightX),"
" mix(%s, %s, repeatCoordWeightX),"
" repeatCoordWeightY);",
read("clampedCoord").c_str(),
read("float2(extraRepeatCoord.x, clampedCoord.y)").c_str(),
read("float2(clampedCoord.x, extraRepeatCoord.y)").c_str(),
read("float2(extraRepeatCoord.x, extraRepeatCoord.y)").c_str());
} else if (mipmapRepeatX) {
fb->codeAppendf("half4 textureColor = mix(%s, %s, repeatCoordWeightX);",
read("clampedCoord").c_str(),
read("float2(extraRepeatCoord.x, clampedCoord.y)").c_str());
} else if (mipmapRepeatY) {
fb->codeAppendf("half4 textureColor = mix(%s, %s, repeatCoordWeightY);",
read("clampedCoord").c_str(),
read("float2(clampedCoord.x, extraRepeatCoord.y)").c_str());
} else {
fb->codeAppendf("half4 textureColor = %s;", read("clampedCoord").c_str());
}
// Strings for extra texture reads used only in kRepeatLinear
SkString repeatLinearReadX;
SkString repeatLinearReadY;
// Calculate the amount the coord moved for clamping. This will be used
// to implement shader-based filtering for kClampToBorder and kRepeat.
bool repeatLinearFilterX = m[0] == ShaderMode::kRepeat_Linear_None ||
m[0] == ShaderMode::kRepeat_Linear_Mipmap;
bool repeatLinearFilterY = m[1] == ShaderMode::kRepeat_Linear_None ||
m[1] == ShaderMode::kRepeat_Linear_Mipmap;
if (repeatLinearFilterX || m[0] == ShaderMode::kClampToBorder_Filter) {
fb->codeAppend("half errX = half(subsetCoord.x - clampedCoord.x);");
if (repeatLinearFilterX) {
fb->codeAppendf("float repeatCoordX = errX > 0 ? %s.x : %s.z;",
clampName, clampName);
repeatLinearReadX = read("float2(repeatCoordX, clampedCoord.y)");
}
}
if (repeatLinearFilterY || m[1] == ShaderMode::kClampToBorder_Filter) {
fb->codeAppend("half errY = half(subsetCoord.y - clampedCoord.y);");
if (repeatLinearFilterY) {
fb->codeAppendf("float repeatCoordY = errY > 0 ? %s.y : %s.w;",
clampName, clampName);
repeatLinearReadY = read("float2(clampedCoord.x, repeatCoordY)");
}
}
// Add logic for kRepeat + linear filter. Do 1 or 3 more texture reads depending
// on whether both modes are kRepeat and whether we're near a single subset edge
// or a corner. Then blend the multiple reads using the err values calculated
// above.
const char* ifStr = "if";
if (repeatLinearFilterX && repeatLinearFilterY) {
auto repeatLinearReadXY = read("float2(repeatCoordX, repeatCoordY)");
fb->codeAppendf(
"if (errX != 0 && errY != 0) {"
" errX = abs(errX);"
" textureColor = mix(mix(textureColor, %s, errX),"
" mix(%s, %s, errX),"
" abs(errY));"
"}",
repeatLinearReadX.c_str(), repeatLinearReadY.c_str(),
repeatLinearReadXY.c_str());
ifStr = "else if";
}
if (repeatLinearFilterX) {
fb->codeAppendf(
"%s (errX != 0) {"
" textureColor = mix(textureColor, %s, abs(errX));"
"}",
ifStr, repeatLinearReadX.c_str());
}
if (repeatLinearFilterY) {
fb->codeAppendf(
"%s (errY != 0) {"
" textureColor = mix(textureColor, %s, abs(errY));"
"}",
ifStr, repeatLinearReadY.c_str());
}
// Do soft edge shader filtering against border color for kClampToBorderFilter using
// the err values calculated above.
if (m[0] == ShaderMode::kClampToBorder_Filter) {
fb->codeAppendf("textureColor = mix(textureColor, %s, min(abs(errX), 1));", borderName);
}
if (m[1] == ShaderMode::kClampToBorder_Filter) {
fb->codeAppendf("textureColor = mix(textureColor, %s, min(abs(errY), 1));", borderName);
}
// Do hard-edge shader transition to border color for kClampToBorderNearest at the
// subset boundaries. Snap the input coordinates to nearest neighbor (with an
// epsilon) before comparing to the subset rect to avoid GPU interpolation errors
if (m[0] == ShaderMode::kClampToBorder_Nearest) {
fb->codeAppendf(
"float snappedX = floor(inCoord.x + 0.001) + 0.5;"
"if (snappedX < %s.x || snappedX > %s.z) {"
" textureColor = %s;"
"}",
subsetName, subsetName, borderName);
}
if (m[1] == ShaderMode::kClampToBorder_Nearest) {
fb->codeAppendf(
"float snappedY = floor(inCoord.y + 0.001) + 0.5;"
"if (snappedY < %s.y || snappedY > %s.w) {"
" textureColor = %s;"
"}",
subsetName, subsetName, borderName);
}
fb->codeAppendf("return textureColor;");
}
}