| // 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/reference.h" |
| #include "src/tint/sem/statement.h" |
| |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::Robustness); |
| TINT_INSTANTIATE_TYPEINFO(tint::transform::Robustness::Config); |
| |
| 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* ret_type = ctx.src->Sem().Get(expr->object)->Type(); |
| |
| auto* ref = ret_type->As<sem::Reference>(); |
| if (ref && omitted_classes.count(ref->StorageClass()) != 0) { |
| return nullptr; |
| } |
| |
| auto* ret_unwrapped = ret_type->UnwrapRef(); |
| |
| ProgramBuilder& b = *ctx.dst; |
| using u32 = ProgramBuilder::u32; |
| |
| 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<sem::Vector>()) { |
| size.u32 = vec->Width(); |
| |
| } else if (auto* arr = ret_unwrapped->As<sem::Array>()) { |
| size.u32 = arr->Count(); |
| } else if (auto* mat = ret_unwrapped->As<sem::Matrix>()) { |
| // 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<sem::Array>()) { |
| 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, 1u); |
| } 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<sem::I32, sem::U32>()) { |
| 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<sem::I32>()) { |
| idx.i32 = idx_constant.Elements()[0].i32; |
| idx.is_signed = true; |
| } else if (idx_constant.Type()->Is<sem::U32>()) { |
| idx.u32 = idx_constant.Elements()[0].u32; |
| 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<sem::I32>(); |
| } |
| |
| // 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<u32>(idx.expr); |
| idx.is_signed = false; |
| } else { |
| idx.u32 = static_cast<uint32_t>(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(idx.u32); |
| } |
| if (!limit.expr) { |
| limit.expr = b.Expr(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<int32_t>( |
| std::min<uint32_t>(limit.u32, std::numeric_limits<int32_t>::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<const ast::Expression*>(b.Expr(idx.i32)) |
| : static_cast<const ast::Expression*>(b.Expr(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<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[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<const ast::Expression*()> 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); |
| auto* max = ctx.dst->Sub(num_levels, 1); |
| 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)); |
| 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); |
| auto* max = ctx.dst->Sub(num_layers, 1); |
| 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)); |
| } |
| |
| 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 |