blob: e662e56b4c69a05173b43245e6d9737364177983 [file] [log] [blame]
// Copyright 2020 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "src/tint/transform/robustness.h"
#include <algorithm>
#include <limits>
#include <utility>
#include "src/tint/program_builder.h"
#include "src/tint/sem/block_statement.h"
#include "src/tint/sem/call.h"
#include "src/tint/sem/expression.h"
#include "src/tint/sem/index_accessor_expression.h"
#include "src/tint/sem/reference.h"
#include "src/tint/sem/statement.h"
TINT_INSTANTIATE_TYPEINFO(tint::transform::Robustness);
TINT_INSTANTIATE_TYPEINFO(tint::transform::Robustness::Config);
using namespace tint::number_suffixes; // NOLINT
namespace tint::transform {
/// State holds the current transform state
struct Robustness::State {
/// The clone context
CloneContext& ctx;
/// Set of storage classes to not apply the transform to
std::unordered_set<ast::StorageClass> omitted_classes;
/// Applies the transformation state to `ctx`.
void Transform() {
ctx.ReplaceAll([&](const ast::IndexAccessorExpression* expr) { return Transform(expr); });
ctx.ReplaceAll([&](const ast::CallExpression* expr) { return Transform(expr); });
}
/// Apply bounds clamping to array, vector and matrix indexing
/// @param expr the array, vector or matrix index expression
/// @return the clamped replacement expression, or nullptr if `expr` should be cloned without
/// changes.
const ast::IndexAccessorExpression* Transform(const ast::IndexAccessorExpression* expr) {
auto* sem =
ctx.src->Sem().Get(expr)->UnwrapMaterialize()->As<sem::IndexAccessorExpression>();
auto* ret_type = sem->Type();
auto* ref = ret_type->As<sem::Reference>();
if (ref && omitted_classes.count(ref->StorageClass()) != 0) {
return nullptr;
}
ProgramBuilder& b = *ctx.dst;
// idx return the cloned index expression, as a u32.
auto idx = [&]() -> const ast::Expression* {
auto* i = ctx.Clone(expr->index);
if (sem->Index()->Type()->UnwrapRef()->is_signed_integer_scalar()) {
return b.Construct(b.ty.u32(), i); // u32(idx)
}
return i;
};
auto* clamped_idx = Switch(
sem->Object()->Type()->UnwrapRef(), //
[&](const sem::Vector* vec) -> const ast::Expression* {
if (sem->Index()->ConstantValue()) {
// Index and size is constant.
// Validation will have rejected any OOB accesses.
return nullptr;
}
return b.Call("min", idx(), u32(vec->Width() - 1u));
},
[&](const sem::Matrix* mat) -> const ast::Expression* {
if (sem->Index()->ConstantValue()) {
// Index and size is constant.
// Validation will have rejected any OOB accesses.
return nullptr;
}
return b.Call("min", idx(), u32(mat->columns() - 1u));
},
[&](const sem::Array* arr) -> const ast::Expression* {
const ast::Expression* max = nullptr;
if (arr->IsRuntimeSized()) {
// Size is unknown until runtime.
// Must clamp, even if the index is constant.
auto* arr_ptr = b.AddressOf(ctx.Clone(expr->object));
max = b.Sub(b.Call("arrayLength", arr_ptr), 1_u);
} else {
if (sem->Index()->ConstantValue()) {
// Index and size is constant.
// Validation will have rejected any OOB accesses.
return nullptr;
}
max = b.Expr(u32(arr->Count() - 1u));
}
return b.Call("min", idx(), max);
},
[&](Default) {
TINT_ICE(Transform, b.Diagnostics())
<< "unhandled object type in robustness of array index: "
<< ctx.src->FriendlyName(ret_type->UnwrapRef());
return nullptr;
});
if (!clamped_idx) {
return nullptr; // Clamping not needed
}
auto src = ctx.Clone(expr->source);
auto* obj = ctx.Clone(expr->object);
return b.IndexAccessor(src, obj, clamped_idx);
}
/// @param type builtin type
/// @returns true if the given builtin is a texture function that requires
/// argument clamping,
bool TextureBuiltinNeedsClamping(sem::BuiltinType type) {
return type == sem::BuiltinType::kTextureLoad || type == sem::BuiltinType::kTextureStore;
}
/// Apply bounds clamping to the coordinates, array index and level arguments
/// of the `textureLoad()` and `textureStore()` builtins.
/// @param expr the builtin call expression
/// @return the clamped replacement call expression, or nullptr if `expr`
/// should be cloned without changes.
const ast::CallExpression* Transform(const ast::CallExpression* expr) {
auto* call = ctx.src->Sem().Get(expr)->UnwrapMaterialize()->As<sem::Call>();
auto* call_target = call->Target();
auto* builtin = call_target->As<sem::Builtin>();
if (!builtin || !TextureBuiltinNeedsClamping(builtin->Type())) {
return nullptr; // No transform, just clone.
}
ProgramBuilder& b = *ctx.dst;
// Indices of the mandatory texture and coords parameters, and the optional
// array and level parameters.
auto& signature = builtin->Signature();
auto texture_idx = signature.IndexOf(sem::ParameterUsage::kTexture);
auto coords_idx = signature.IndexOf(sem::ParameterUsage::kCoords);
auto array_idx = signature.IndexOf(sem::ParameterUsage::kArrayIndex);
auto level_idx = signature.IndexOf(sem::ParameterUsage::kLevel);
auto* texture_arg = expr->args[static_cast<size_t>(texture_idx)];
auto* coords_arg = expr->args[static_cast<size_t>(coords_idx)];
auto* coords_ty = builtin->Parameters()[static_cast<size_t>(coords_idx)]->Type();
// If the level is provided, then we need to clamp this. As the level is
// used by textureDimensions() and the texture[Load|Store]() calls, we need
// to clamp both usages.
// TODO(bclayton): We probably want to place this into a let so that the
// calculation can be reused. This is fiddly to get right.
std::function<const ast::Expression*()> level_arg;
if (level_idx >= 0) {
level_arg = [&] {
auto* arg = expr->args[static_cast<size_t>(level_idx)];
auto* num_levels = b.Call("textureNumLevels", ctx.Clone(texture_arg));
auto* zero = b.Expr(0_i);
auto* max = ctx.dst->Sub(num_levels, 1_i);
auto* clamped = b.Call("clamp", ctx.Clone(arg), zero, max);
return clamped;
};
}
// Clamp the coordinates argument
{
auto* texture_dims =
level_arg ? b.Call("textureDimensions", ctx.Clone(texture_arg), level_arg())
: b.Call("textureDimensions", ctx.Clone(texture_arg));
auto* zero = b.Construct(CreateASTTypeFor(ctx, coords_ty));
auto* max =
ctx.dst->Sub(texture_dims, b.Construct(CreateASTTypeFor(ctx, coords_ty), 1_i));
auto* clamped_coords = b.Call("clamp", ctx.Clone(coords_arg), zero, max);
ctx.Replace(coords_arg, clamped_coords);
}
// Clamp the array_index argument, if provided
if (array_idx >= 0) {
auto* arg = expr->args[static_cast<size_t>(array_idx)];
auto* num_layers = b.Call("textureNumLayers", ctx.Clone(texture_arg));
auto* zero = b.Expr(0_i);
auto* max = ctx.dst->Sub(num_layers, 1_i);
auto* clamped = b.Call("clamp", ctx.Clone(arg), zero, max);
ctx.Replace(arg, clamped);
}
// Clamp the level argument, if provided
if (level_idx >= 0) {
auto* arg = expr->args[static_cast<size_t>(level_idx)];
ctx.Replace(arg, level_arg ? level_arg() : ctx.dst->Expr(0_i));
}
return nullptr; // Clone, which will use the argument replacements above.
}
};
Robustness::Config::Config() = default;
Robustness::Config::Config(const Config&) = default;
Robustness::Config::~Config() = default;
Robustness::Config& Robustness::Config::operator=(const Config&) = default;
Robustness::Robustness() = default;
Robustness::~Robustness() = default;
void Robustness::Run(CloneContext& ctx, const DataMap& inputs, DataMap&) const {
Config cfg;
if (auto* cfg_data = inputs.Get<Config>()) {
cfg = *cfg_data;
}
std::unordered_set<ast::StorageClass> omitted_classes;
for (auto sc : cfg.omitted_classes) {
switch (sc) {
case StorageClass::kUniform:
omitted_classes.insert(ast::StorageClass::kUniform);
break;
case StorageClass::kStorage:
omitted_classes.insert(ast::StorageClass::kStorage);
break;
}
}
State state{ctx, std::move(omitted_classes)};
state.Transform();
ctx.Clone();
}
} // namespace tint::transform