// 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 #include #include #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/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 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* ret_type = ctx.src->Sem().Get(expr->object)->Type(); auto* ref = ret_type->As(); if (ref && omitted_classes.count(ref->StorageClass()) != 0) { return nullptr; } auto* ret_unwrapped = ret_type->UnwrapRef(); ProgramBuilder& b = *ctx.dst; struct Value { const ast::Expression* expr = nullptr; // If null, then is a constant union { uint32_t u32 = 0; // use if is_signed == false int32_t i32; // use if is_signed == true }; bool is_signed = false; }; Value size; // size of the array, vector or matrix size.is_signed = false; // size is always unsigned if (auto* vec = ret_unwrapped->As()) { size.u32 = vec->Width(); } else if (auto* arr = ret_unwrapped->As()) { size.u32 = arr->Count(); } else if (auto* mat = ret_unwrapped->As()) { // The row accessor would have been an embedded index accessor and already // handled, so we just need to do columns here. size.u32 = mat->columns(); } else { return nullptr; } if (size.u32 == 0) { if (!ret_unwrapped->Is()) { b.Diagnostics().add_error(diag::System::Transform, "invalid 0 sized non-array", expr->source); return nullptr; } // Runtime sized array auto* arr = ctx.Clone(expr->object); size.expr = b.Call("arrayLength", b.AddressOf(arr)); } // Calculate the maximum possible index value (size-1u) // Size must be positive (non-zero), so we can safely subtract 1 here // without underflow. Value limit; limit.is_signed = false; // Like size, limit is always unsigned. if (size.expr) { // Dynamic size limit.expr = b.Sub(size.expr, 1_u); } else { // Constant size limit.u32 = size.u32 - 1u; } Value idx; // index value auto* idx_sem = ctx.src->Sem().Get(expr->index); auto* idx_ty = idx_sem->Type()->UnwrapRef(); if (!idx_ty->IsAnyOf()) { TINT_ICE(Transform, b.Diagnostics()) << "index must be u32 or i32, got " << idx_sem->Type()->TypeInfo().name; return nullptr; } if (auto idx_constant = idx_sem->ConstantValue()) { // Constant value index if (idx_constant.Type()->Is()) { idx.i32 = static_cast(idx_constant.Element(0).value); idx.is_signed = true; } else if (idx_constant.Type()->Is()) { idx.u32 = static_cast(idx_constant.Element(0).value); idx.is_signed = false; } else { TINT_ICE(Transform, b.Diagnostics()) << "unsupported constant value for accessor " << idx_constant.Type()->TypeInfo().name; return nullptr; } } else { // Dynamic value index idx.expr = ctx.Clone(expr->index); idx.is_signed = idx_ty->Is(); } // Clamp the index so that it cannot exceed limit. if (idx.expr || limit.expr) { // One of, or both of idx and limit are non-constant. // If the index is signed, cast it to a u32 (with clamping if constant). if (idx.is_signed) { if (idx.expr) { // We don't use a max(idx, 0) here, as that incurs a runtime // performance cost, and if the unsigned value will be clamped by // limit, resulting in a value between [0..limit) idx.expr = b.Construct(idx.expr); idx.is_signed = false; } else { idx.u32 = static_cast(std::max(idx.i32, 0)); idx.is_signed = false; } } // Convert idx and limit to expressions, so we can emit `min(idx, limit)`. if (!idx.expr) { idx.expr = b.Expr(u32(idx.u32)); } if (!limit.expr) { limit.expr = b.Expr(u32(limit.u32)); } // Perform the clamp with `min(idx, limit)` idx.expr = b.Call("min", idx.expr, limit.expr); } else { // Both idx and max are constant. if (idx.is_signed) { // The index is signed. Calculate limit as signed. int32_t signed_limit = static_cast( std::min(limit.u32, std::numeric_limits::max())); idx.i32 = std::max(idx.i32, 0); idx.i32 = std::min(idx.i32, signed_limit); } else { // The index is unsigned. idx.u32 = std::min(idx.u32, limit.u32); } } // Convert idx to an expression, so we can emit the new accessor. if (!idx.expr) { idx.expr = idx.is_signed ? static_cast(b.Expr(i32(idx.i32))) : static_cast(b.Expr(u32(idx.u32))); } // Clone arguments outside of create() call to have deterministic ordering auto src = ctx.Clone(expr->source); auto* obj = ctx.Clone(expr->object); return b.IndexAccessor(src, obj, idx.expr); } /// @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); auto* call_target = call->Target(); auto* builtin = call_target->As(); 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[texture_idx]; auto* coords_arg = expr->args[coords_idx]; auto* coords_ty = builtin->Parameters()[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 level_arg; if (level_idx >= 0) { level_arg = [&] { auto* arg = expr->args[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[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[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()) { cfg = *cfg_data; } std::unordered_set 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