blob: cbfe3ce52e7a34081ba646770cc27798785a03d9 [file] [log] [blame]
/*
* Copyright 2017 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/effects/GrTextureEffect.h"
#include "include/gpu/GrTexture.h"
#include "src/gpu/GrTexturePriv.h"
#include "src/gpu/glsl/GrGLSLFragmentProcessor.h"
#include "src/gpu/glsl/GrGLSLFragmentShaderBuilder.h"
#include "src/gpu/glsl/GrGLSLProgramBuilder.h"
#include "src/sksl/SkSLCPP.h"
#include "src/sksl/SkSLUtil.h"
namespace {
struct Span {
float fA = 0.f, fB = 0.f;
Span makeInset(float o) const {
Span r = {fA + o, fB - o};
if (r.fA > r.fB) {
r.fA = r.fB = (r.fA + r.fB) / 2;
}
return r;
}
bool contains(Span r) const { return fA <= r.fA && fB >= r.fB; }
};
} // anonymous namespace
GrTextureEffect::Sampling::Sampling(GrSamplerState sampler, SkISize size, const GrCaps& caps)
: fHWSampler(sampler) {
if (!caps.clampToBorderSupport()) {
if (fHWSampler.wrapModeX() == GrSamplerState::WrapMode::kClampToBorder) {
fHWSampler.setWrapModeX(GrSamplerState::WrapMode::kClamp);
fShaderModes[0] = ShaderMode::kDecal;
Span span{0, (float)size.width()};
if (sampler.filter() != GrSamplerState::Filter::kNearest) {
span = span.makeInset(0.5f);
}
fShaderSubset.fLeft = span.fA;
fShaderSubset.fRight = span.fB;
}
if (fHWSampler.wrapModeY() == GrSamplerState::WrapMode::kClampToBorder) {
fHWSampler.setWrapModeY(GrSamplerState::WrapMode::kClamp);
fShaderModes[1] = ShaderMode::kDecal;
Span span{0, (float)size.height()};
if (sampler.filter() != GrSamplerState::Filter::kNearest) {
span = span.makeInset(0.5f);
}
fShaderSubset.fTop = span.fA;
fShaderSubset.fBottom = span.fB;
}
}
if (!caps.npotTextureTileSupport()) {
if (fHWSampler.wrapModeX() != GrSamplerState::WrapMode::kClamp && !SkIsPow2(size.width())) {
fShaderModes[0] = static_cast<ShaderMode>(fHWSampler.wrapModeX());
fHWSampler.setWrapModeX(GrSamplerState::WrapMode::kClamp);
// We don't yet support shader based Mirror or Repeat with filtering.
fHWSampler.setFilterMode(GrSamplerState::Filter::kNearest);
fShaderSubset.fLeft = 0;
fShaderSubset.fRight = size.width();
}
if (fHWSampler.wrapModeY() != GrSamplerState::WrapMode::kClamp &&
!SkIsPow2(size.height())) {
fShaderModes[1] = static_cast<ShaderMode>(fHWSampler.wrapModeY());
fHWSampler.setWrapModeY(GrSamplerState::WrapMode::kClamp);
fHWSampler.setFilterMode(GrSamplerState::Filter::kNearest);
fShaderSubset.fTop = 0;
fShaderSubset.fBottom = size.height();
}
}
}
GrTextureEffect::Sampling::Sampling(const GrSurfaceProxy& proxy,
GrSamplerState sampler,
const SkRect& subset,
const SkRect* domain,
const GrCaps& caps) {
using Mode = GrSamplerState::WrapMode;
using Filter = GrSamplerState::Filter;
struct Result1D {
ShaderMode fShaderMode;
Span fShaderSubset;
Mode fHWMode;
Filter fFilter;
};
auto resolve = [filter = sampler.filter(), &caps](int size, Mode mode, Span subset,
Span domain) {
Result1D r;
r.fFilter = filter;
bool canDoHW = (mode != Mode::kClampToBorder || caps.clampToBorderSupport()) &&
(mode == Mode::kClamp || caps.npotTextureTileSupport() || SkIsPow2(size));
if (canDoHW && size > 0 && subset.fA <= 0 && subset.fB >= size) {
r.fShaderMode = ShaderMode::kNone;
r.fHWMode = mode;
return r;
}
bool domainIsSafe = false;
Span insetSubset;
if (filter == Filter::kNearest) {
Span isubset{sk_float_floor(subset.fA), sk_float_ceil(subset.fB)};
if (domain.fA > isubset.fA && domain.fB < isubset.fB) {
domainIsSafe = true;
}
if (mode == Mode::kClamp) {
// This inset prevents sampling neighboring texels that could occur when
// texture coords fall exactly at texel boundaries (depending on precision
// and GPU-specific snapping at the boundary).
insetSubset = isubset.makeInset(0.5f);
} else {
// TODO: Handle other modes properly in this case.
insetSubset = subset;
}
} else {
insetSubset = subset.makeInset(0.5f);
domainIsSafe = insetSubset.contains(domain);
}
if (canDoHW && domainIsSafe) {
r.fShaderMode = ShaderMode::kNone;
r.fHWMode = mode;
return r;
}
if (mode == Mode::kRepeat || mode == Mode::kMirrorRepeat) {
r.fFilter = Filter::kNearest;
r.fShaderSubset = subset;
} else {
r.fShaderSubset = insetSubset;
}
r.fShaderMode = static_cast<ShaderMode>(mode);
r.fHWMode = Mode::kClamp;
return r;
};
SkISize dim = proxy.isFullyLazy() ? SkISize{-1, -1} : proxy.backingStoreDimensions();
Span subsetX{subset.fLeft, subset.fRight};
auto domainX = domain ? Span{domain->fLeft, domain->fRight}
: Span{SK_FloatNegativeInfinity, SK_FloatInfinity};
auto x = resolve(dim.width(), sampler.wrapModeX(), subsetX, domainX);
Span subsetY{subset.fTop, subset.fBottom};
auto domainY = domain ? Span{domain->fTop, domain->fBottom}
: Span{SK_FloatNegativeInfinity, SK_FloatInfinity};
auto y = resolve(dim.height(), sampler.wrapModeY(), subsetY, domainY);
fHWSampler = {x.fHWMode, y.fHWMode, std::min(x.fFilter, y.fFilter)};
fShaderModes[0] = x.fShaderMode;
fShaderModes[1] = y.fShaderMode;
fShaderSubset = {x.fShaderSubset.fA, y.fShaderSubset.fA,
x.fShaderSubset.fB, y.fShaderSubset.fB};
}
bool GrTextureEffect::Sampling::usesDecal() const {
return fShaderModes[0] == ShaderMode::kDecal || fShaderModes[1] == ShaderMode::kDecal ||
fHWSampler.wrapModeX() == GrSamplerState::WrapMode::kClampToBorder ||
fHWSampler.wrapModeY() == GrSamplerState::WrapMode::kClampToBorder;
}
std::unique_ptr<GrFragmentProcessor> GrTextureEffect::Make(GrSurfaceProxyView view,
SkAlphaType alphaType,
const SkMatrix& matrix,
GrSamplerState::Filter filter) {
return std::unique_ptr<GrFragmentProcessor>(
new GrTextureEffect(std::move(view), alphaType, matrix, Sampling(filter)));
}
std::unique_ptr<GrFragmentProcessor> GrTextureEffect::Make(GrSurfaceProxyView view,
SkAlphaType alphaType,
const SkMatrix& matrix,
GrSamplerState sampler,
const GrCaps& caps) {
Sampling sampling(sampler, view.proxy()->dimensions(), caps);
return std::unique_ptr<GrFragmentProcessor>(
new GrTextureEffect(std::move(view), alphaType, matrix, sampling));
}
std::unique_ptr<GrFragmentProcessor> GrTextureEffect::MakeTexelSubset(GrSurfaceProxyView view,
SkAlphaType alphaType,
const SkMatrix& matrix,
GrSamplerState sampler,
const SkIRect& subset,
const GrCaps& caps) {
Sampling sampling(*view.proxy(), sampler, SkRect::Make(subset), nullptr, caps);
return std::unique_ptr<GrFragmentProcessor>(
new GrTextureEffect(std::move(view), alphaType, matrix, sampling));
}
std::unique_ptr<GrFragmentProcessor> GrTextureEffect::MakeTexelSubset(GrSurfaceProxyView view,
SkAlphaType alphaType,
const SkMatrix& matrix,
GrSamplerState sampler,
const SkIRect& subset,
const SkRect& domain,
const GrCaps& caps) {
Sampling sampling(*view.proxy(), sampler, SkRect::Make(subset), &domain, caps);
return std::unique_ptr<GrFragmentProcessor>(
new GrTextureEffect(std::move(view), alphaType, matrix, sampling));
}
std::unique_ptr<GrFragmentProcessor> GrTextureEffect::MakeSubset(GrSurfaceProxyView view,
SkAlphaType alphaType,
const SkMatrix& matrix,
GrSamplerState sampler,
const SkRect& subset,
const GrCaps& caps) {
Sampling sampling(*view.proxy(), sampler, subset, nullptr, caps);
return std::unique_ptr<GrFragmentProcessor>(
new GrTextureEffect(std::move(view), alphaType, matrix, sampling));
}
std::unique_ptr<GrFragmentProcessor> GrTextureEffect::MakeSubset(GrSurfaceProxyView view,
SkAlphaType alphaType,
const SkMatrix& matrix,
GrSamplerState sampler,
const SkRect& subset,
const SkRect& domain,
const GrCaps& caps) {
Sampling sampling(*view.proxy(), sampler, subset, &domain, caps);
return std::unique_ptr<GrFragmentProcessor>(
new GrTextureEffect(std::move(view), alphaType, matrix, sampling));
}
GrGLSLFragmentProcessor* GrTextureEffect::onCreateGLSLInstance() const {
class Impl : public GrGLSLFragmentProcessor {
UniformHandle fSubsetUni;
UniformHandle fDecalUni;
public:
void emitCode(EmitArgs& args) override {
auto appendWrap = [](GrGLSLShaderBuilder* builder, ShaderMode mode, const char* inCoord,
const char* domainStart, const char* domainEnd, bool is2D,
const char* out) {
switch (mode) {
case ShaderMode::kNone:
builder->codeAppendf("%s = %s;\n", out, inCoord);
break;
case ShaderMode::kDecal:
// The lookup coordinate to use for decal will be clamped just like
// kClamp_Mode, it's just that the post-processing will be different, so
// fall through
case ShaderMode::kClamp:
builder->codeAppendf("%s = clamp(%s, %s, %s);", out, inCoord, domainStart,
domainEnd);
break;
case ShaderMode::kRepeat:
builder->codeAppendf("%s = mod(%s - %s, %s - %s) + %s;", out, inCoord,
domainStart, domainEnd, domainStart, domainStart);
break;
case ShaderMode::kMirrorRepeat: {
const char* type = is2D ? "float2" : "float";
builder->codeAppend("{");
builder->codeAppendf("%s w = %s - %s;", type, domainEnd, domainStart);
builder->codeAppendf("%s w2 = 2 * w;", type);
builder->codeAppendf("%s m = mod(%s - %s, w2);", type, inCoord,
domainStart);
builder->codeAppendf("%s = mix(m, w2 - m, step(w, m)) + %s;", out,
domainStart);
builder->codeAppend("}");
break;
}
}
};
auto te = args.fFp.cast<GrTextureEffect>();
const char* coords;
if (args.fFp.coordTransformsApplyToLocalCoords()) {
coords = args.fTransformedCoords[0].fVaryingPoint.c_str();
} else {
coords = "_coords";
}
auto* fb = args.fFragBuilder;
if (te.fShaderModes[0] == ShaderMode::kNone &&
te.fShaderModes[1] == ShaderMode::kNone) {
fb->codeAppendf("%s = ", args.fOutputColor);
fb->appendTextureLookupAndBlend(args.fInputColor, SkBlendMode::kModulate,
args.fTexSamplers[0], coords);
fb->codeAppendf(";");
} else {
const char* subsetName;
SkString uniName("TexDom");
fSubsetUni = args.fUniformHandler->addUniform(
kFragment_GrShaderFlag, kHalf4_GrSLType, "subset", &subsetName);
// Always use a local variable for the input coordinates; often callers pass in an
// expression and we want to cache it across all of its references in the code below
auto inCoords = fb->ensureCoords2D(args.fTransformedCoords[0].fVaryingPoint);
fb->codeAppend("float2 clampedCoord;");
SkString start;
SkString end;
if (te.fShaderModes[0] == te.fShaderModes[1]) {
// Doing the domain setup using vectors seems to avoid shader compilation issues
// on Chromecast, possibly due to reducing shader length.
start.printf("%s.xy", subsetName);
end.printf("%s.zw", subsetName);
appendWrap(fb, te.fShaderModes[0], inCoords.c_str(), start.c_str(), end.c_str(),
true, "clampedCoord");
} else {
SkString origX, origY;
// Apply x mode to the x coordinate using the left and right edges of the domain
// rect (stored as the x and z components of the domain uniform).
start.printf("%s.x", subsetName);
end.printf("%s.z", subsetName);
origX.printf("%s.x", inCoords.c_str());
appendWrap(fb, te.fShaderModes[0], origX.c_str(), start.c_str(), end.c_str(),
false, "clampedCoord.x");
// Repeat the same logic for y.
start.printf("%s.y", subsetName);
end.printf("%s.w", subsetName);
origY.printf("%s.y", inCoords.c_str());
appendWrap(fb, te.fShaderModes[1], origY.c_str(), start.c_str(), end.c_str(),
false, "clampedCoord.y");
}
SkString textureLookup;
fb->appendTextureLookup(&textureLookup, args.fTexSamplers[0], "clampedCoord");
fb->codeAppendf("half4 textureColor = %s;", textureLookup.c_str());
// Apply decal mode's transparency interpolation if needed
bool decalX = te.fShaderModes[0] == ShaderMode::kDecal;
bool decalY = te.fShaderModes[1] == ShaderMode::kDecal;
if (decalX || decalY) {
const char* decalName;
// Half3 since this will hold texture width, height, and then a step function
// control param
fDecalUni = args.fUniformHandler->addUniform(
kFragment_GrShaderFlag, kHalf3_GrSLType, uniName.c_str(), &decalName);
// The decal err is the max absolute value between the clamped coordinate and
// the original pixel coordinate. This will then be clamped to 1.f if it's
// greater than the control parameter, which simulates kNearest and kBilerp
// behavior depending on if it's 0 or 1.
if (decalX && decalY) {
fb->codeAppendf(
"half err = max(half(abs(clampedCoord.x - %s.x) * %s.x), "
" half(abs(clampedCoord.y - %s.y) * %s.y));",
inCoords.c_str(), decalName, inCoords.c_str(), decalName);
} else if (decalX) {
fb->codeAppendf("half err = half(abs(clampedCoord.x - %s.x) * %s.x);",
inCoords.c_str(), decalName);
} else {
SkASSERT(decalY);
fb->codeAppendf("half err = half(abs(clampedCoord.y - %s.y) * %s.y);",
inCoords.c_str(), decalName);
}
// Apply a transform to the error rate, which let's us simulate nearest or
// bilerp filtering in the same shader. When the texture is nearest filtered,
// fSizeName.z is set to 0 so this becomes a step function centered at the
// clamped coordinate. When bilerp, fSizeName.z is set to 1 and it becomes
// a simple linear blend between texture and transparent.
fb->codeAppendf(
"if (err > %s.z) { err = 1.0; } else if (%s.z < 1) { err = 0.0; }",
decalName, decalName);
fb->codeAppend("textureColor = mix(textureColor, half4(0), err);");
}
fb->codeAppendf("%s = textureColor * %s;", args.fOutputColor, args.fInputColor);
}
}
protected:
void onSetData(const GrGLSLProgramDataManager& pdm,
const GrFragmentProcessor& fp) override {
const auto& te = fp.cast<GrTextureEffect>();
if (fSubsetUni.isValid()) {
const float w = te.fSampler.peekTexture()->width();
const float h = te.fSampler.peekTexture()->height();
const auto& s = te.fSubset;
float rect[] = {s.fLeft, s.fTop, s.fRight, s.fBottom};
float decalW[3];
if (te.fSampler.view().origin() == kBottomLeft_GrSurfaceOrigin) {
rect[1] = h - rect[1];
rect[3] = h - rect[3];
std::swap(rect[1], rect[3]);
}
if (te.fSampler.peekTexture()->texturePriv().textureType() !=
GrTextureType::kRectangle) {
float iw = 1.f / w;
float ih = 1.f / h;
rect[0] *= iw;
rect[2] *= iw;
rect[1] *= ih;
rect[3] *= ih;
decalW[0] = w;
decalW[1] = h;
} else {
decalW[0] = 1;
decalW[1] = 1;
}
pdm.set4fv(fSubsetUni, 1, rect);
if (fDecalUni.isValid()) {
bool filter = te.textureSampler(0).samplerState().filter() !=
GrSamplerState::Filter::kNearest;
decalW[2] = filter ? 1.f : 0.f;
pdm.set3fv(fDecalUni, 1, decalW);
}
}
}
};
return new Impl;
}
void GrTextureEffect::onGetGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const {
bool shaderFilter = (fShaderModes[0] == ShaderMode::kDecal ||
fShaderModes[1] == ShaderMode::kDecal) &&
fSampler.samplerState().filter() != GrSamplerState::Filter::kNearest;
auto m0 = static_cast<uint32_t>(fShaderModes[0]);
auto m1 = static_cast<uint32_t>(fShaderModes[1]);
b->add32(shaderFilter << 31 | (m0 << 16) | m1);
}
bool GrTextureEffect::onIsEqual(const GrFragmentProcessor& other) const {
auto that = other.cast<GrTextureEffect>();
return fShaderModes[0] == that.fShaderModes[1] && fShaderModes[1] == that.fShaderModes[1] &&
fSubset == that.fSubset;
}
GrTextureEffect::GrTextureEffect(GrSurfaceProxyView view, SkAlphaType alphaType,
const SkMatrix& matrix, const Sampling& sampling)
: GrFragmentProcessor(kGrTextureEffect_ClassID,
ModulateForSamplerOptFlags(alphaType, sampling.usesDecal()))
, fCoordTransform(matrix, view.proxy())
, fSampler(std::move(view), sampling.fHWSampler)
, fSubset(sampling.fShaderSubset)
, fShaderModes{sampling.fShaderModes[0], sampling.fShaderModes[1]} {
// We always compare the range even when it isn't used so assert we have canonical don't care
// values.
SkASSERT(fShaderModes[0] != ShaderMode::kNone || (fSubset.fLeft == 0 && fSubset.fRight == 0));
SkASSERT(fShaderModes[1] != ShaderMode::kNone || (fSubset.fTop == 0 && fSubset.fBottom == 0));
this->setTextureSamplerCnt(1);
this->addCoordTransform(&fCoordTransform);
}
GrTextureEffect::GrTextureEffect(const GrTextureEffect& src)
: INHERITED(kGrTextureEffect_ClassID, src.optimizationFlags())
, fCoordTransform(src.fCoordTransform)
, fSampler(src.fSampler)
, fSubset(src.fSubset)
, fShaderModes{src.fShaderModes[0], src.fShaderModes[1]} {
this->setTextureSamplerCnt(1);
this->addCoordTransform(&fCoordTransform);
}
std::unique_ptr<GrFragmentProcessor> GrTextureEffect::clone() const {
return std::unique_ptr<GrFragmentProcessor>(new GrTextureEffect(*this));
}
const GrFragmentProcessor::TextureSampler& GrTextureEffect::onTextureSampler(int) const {
return fSampler;
}
GR_DEFINE_FRAGMENT_PROCESSOR_TEST(GrTextureEffect);
#if GR_TEST_UTILS
std::unique_ptr<GrFragmentProcessor> GrTextureEffect::TestCreate(GrProcessorTestData* testData) {
auto [view, ct, at] = testData->randomView();
GrSamplerState::WrapMode wrapModes[2];
GrTest::TestWrapModes(testData->fRandom, wrapModes);
if (!testData->caps()->npotTextureTileSupport()) {
// Performing repeat sampling on npot textures will cause asserts on HW
// that lacks support.
wrapModes[0] = GrSamplerState::WrapMode::kClamp;
wrapModes[1] = GrSamplerState::WrapMode::kClamp;
}
GrSamplerState params(wrapModes, testData->fRandom->nextBool()
? GrSamplerState::Filter::kBilerp
: GrSamplerState::Filter::kNearest);
const SkMatrix& matrix = GrTest::TestMatrix(testData->fRandom);
return GrTextureEffect::Make(std::move(view), at, matrix, params, *testData->caps());
}
#endif