| // 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/statement.h" |
| #include "src/tint/type/reference.h" |
| |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::Robustness); |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::Robustness::Config); |
| |
| using namespace tint::number_suffixes; // NOLINT |
| |
| namespace tint::transform { |
| |
| /// PIMPL state for the transform |
| struct Robustness::State { |
| /// Constructor |
| /// @param program the source program |
| /// @param omitted the omitted address spaces |
| State(const Program* program, std::unordered_set<ast::AddressSpace>&& omitted) |
| : src(program), omitted_address_spaces(std::move(omitted)) {} |
| |
| /// Runs the transform |
| /// @returns the new program or SkipTransform if the transform is not required |
| ApplyResult Run() { |
| ctx.ReplaceAll([&](const ast::IndexAccessorExpression* expr) { return Transform(expr); }); |
| ctx.ReplaceAll([&](const ast::CallExpression* expr) { return Transform(expr); }); |
| |
| ctx.Clone(); |
| return Program(std::move(b)); |
| } |
| |
| private: |
| /// The source program |
| const Program* const src; |
| /// The target program builder |
| ProgramBuilder b; |
| /// The clone context |
| CloneContext ctx = {&b, src, /* auto_clone_symbols */ true}; |
| |
| /// Set of address spaces to not apply the transform to |
| std::unordered_set<ast::AddressSpace> omitted_address_spaces; |
| |
| /// 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 = src->Sem().Get(expr)->Unwrap()->As<sem::IndexAccessorExpression>(); |
| auto* ret_type = sem->Type(); |
| |
| auto* ref = ret_type->As<type::Reference>(); |
| if (ref && omitted_address_spaces.count(ref->AddressSpace()) != 0) { |
| return nullptr; |
| } |
| |
| // idx return the cloned index expression, as a u32. |
| auto idx = [&]() -> const ast::Expression* { |
| auto* i = ctx.Clone(expr->index); |
| if (sem->Index()->Type()->is_signed_integer_scalar()) { |
| return b.Construct(b.ty.u32(), i); // u32(idx) |
| } |
| return i; |
| }; |
| |
| auto* clamped_idx = Switch( |
| sem->Object()->Type()->UnwrapRef(), // |
| [&](const type::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 type::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 type::Array* arr) -> const ast::Expression* { |
| const ast::Expression* max = nullptr; |
| if (arr->Count()->Is<type::RuntimeArrayCount>()) { |
| // 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 (auto count = arr->ConstantCount()) { |
| if (sem->Index()->ConstantValue()) { |
| // Index and size is constant. |
| // Validation will have rejected any OOB accesses. |
| return nullptr; |
| } |
| max = b.Expr(u32(count.value() - 1u)); |
| } else { |
| // Note: Don't be tempted to use the array override variable as an expression |
| // here, the name might be shadowed! |
| b.Diagnostics().add_error(diag::System::Transform, |
| type::Array::kErrExpectedConstantCount); |
| return nullptr; |
| } |
| |
| return b.Call("min", idx(), max); |
| }, |
| [&](Default) { |
| TINT_ICE(Transform, b.Diagnostics()) |
| << "unhandled object type in robustness of array index: " |
| << src->FriendlyName(ret_type->UnwrapRef()); |
| return nullptr; |
| }); |
| |
| if (!clamped_idx) { |
| return nullptr; // Clamping not needed |
| } |
| |
| auto idx_src = ctx.Clone(expr->source); |
| auto* idx_obj = ctx.Clone(expr->object); |
| return b.IndexAccessor(idx_src, idx_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 = 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. |
| } |
| |
| // 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(); |
| |
| auto width_of = [&](const type::Type* ty) { |
| if (auto* vec = ty->As<type::Vector>()) { |
| return vec->Width(); |
| } |
| return 1u; |
| }; |
| auto scalar_or_vec_ty = [&](const ast::Type* scalar, uint32_t width) -> const ast::Type* { |
| if (width > 1) { |
| return b.ty.vec(scalar, width); |
| } |
| return scalar; |
| }; |
| auto scalar_or_vec = [&](const ast::Expression* scalar, |
| uint32_t width) -> const ast::Expression* { |
| if (width > 1) { |
| return b.Construct(b.ty.vec(nullptr, width), scalar); |
| } |
| return scalar; |
| }; |
| auto cast_to_signed = [&](const ast::Expression* val, uint32_t width) { |
| return b.Construct(scalar_or_vec_ty(b.ty.i32(), width), val); |
| }; |
| auto cast_to_unsigned = [&](const ast::Expression* val, uint32_t width) { |
| return b.Construct(scalar_or_vec_ty(b.ty.u32(), width), val); |
| }; |
| |
| // 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 = [&] { |
| const auto* arg = expr->args[static_cast<size_t>(level_idx)]; |
| const auto* target_ty = |
| builtin->Parameters()[static_cast<size_t>(level_idx)]->Type(); |
| const auto* num_levels = b.Call("textureNumLevels", ctx.Clone(texture_arg)); |
| |
| // TODO(crbug.com/tint/1526) remove when num_levels returns u32 |
| num_levels = cast_to_unsigned(num_levels, 1u); |
| |
| const auto* unsigned_max = b.Sub(num_levels, 1_a); |
| if (target_ty->is_signed_integer_scalar()) { |
| const auto* signed_max = cast_to_signed(unsigned_max, 1u); |
| return b.Call("clamp", ctx.Clone(arg), 0_a, signed_max); |
| } else { |
| return b.Call("min", ctx.Clone(arg), unsigned_max); |
| } |
| }; |
| } |
| |
| // Clamp the coordinates argument |
| { |
| const auto* target_ty = coords_ty; |
| const auto width = width_of(target_ty); |
| const auto* texture_dims = |
| level_arg ? b.Call("textureDimensions", ctx.Clone(texture_arg), level_arg()) |
| : b.Call("textureDimensions", ctx.Clone(texture_arg)); |
| |
| // TODO(crbug.com/tint/1526) remove when texture_dims returns u32 or vecN<u32> |
| texture_dims = cast_to_unsigned(texture_dims, width); |
| |
| // texture_dims is u32 or vecN<u32> |
| const auto* unsigned_max = b.Sub(texture_dims, scalar_or_vec(b.Expr(1_a), width)); |
| if (target_ty->is_signed_integer_scalar_or_vector()) { |
| const auto* zero = scalar_or_vec(b.Expr(0_a), width); |
| const auto* signed_max = cast_to_signed(unsigned_max, width); |
| ctx.Replace(coords_arg, b.Call("clamp", ctx.Clone(coords_arg), zero, signed_max)); |
| } else { |
| ctx.Replace(coords_arg, b.Call("min", ctx.Clone(coords_arg), unsigned_max)); |
| } |
| } |
| |
| // Clamp the array_index argument, if provided |
| if (array_idx >= 0) { |
| auto* target_ty = builtin->Parameters()[static_cast<size_t>(array_idx)]->Type(); |
| auto* arg = expr->args[static_cast<size_t>(array_idx)]; |
| auto* num_layers = b.Call("textureNumLayers", ctx.Clone(texture_arg)); |
| |
| // TODO(crbug.com/tint/1526) remove when num_layers returns u32 |
| num_layers = cast_to_unsigned(num_layers, 1u); |
| |
| const auto* unsigned_max = b.Sub(num_layers, 1_a); |
| if (target_ty->is_signed_integer_scalar()) { |
| const auto* signed_max = cast_to_signed(unsigned_max, 1u); |
| ctx.Replace(arg, b.Call("clamp", ctx.Clone(arg), 0_a, signed_max)); |
| } else { |
| ctx.Replace(arg, b.Call("min", ctx.Clone(arg), unsigned_max)); |
| } |
| } |
| |
| // 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() : b.Expr(0_a)); |
| } |
| |
| 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; |
| |
| Transform::ApplyResult Robustness::Apply(const Program* src, |
| const DataMap& inputs, |
| DataMap&) const { |
| Config cfg; |
| if (auto* cfg_data = inputs.Get<Config>()) { |
| cfg = *cfg_data; |
| } |
| |
| std::unordered_set<ast::AddressSpace> omitted_address_spaces; |
| for (auto sc : cfg.omitted_address_spaces) { |
| switch (sc) { |
| case AddressSpace::kUniform: |
| omitted_address_spaces.insert(ast::AddressSpace::kUniform); |
| break; |
| case AddressSpace::kStorage: |
| omitted_address_spaces.insert(ast::AddressSpace::kStorage); |
| break; |
| } |
| } |
| |
| return State{src, std::move(omitted_address_spaces)}.Run(); |
| } |
| |
| } // namespace tint::transform |