1 // Copyright 2020 The Tint Authors.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 #include "src/transform/robustness.h"
16
17 #include <algorithm>
18 #include <limits>
19 #include <utility>
20
21 #include "src/program_builder.h"
22 #include "src/sem/block_statement.h"
23 #include "src/sem/call.h"
24 #include "src/sem/expression.h"
25 #include "src/sem/reference_type.h"
26 #include "src/sem/statement.h"
27
28 TINT_INSTANTIATE_TYPEINFO(tint::transform::Robustness);
29 TINT_INSTANTIATE_TYPEINFO(tint::transform::Robustness::Config);
30
31 namespace tint {
32 namespace transform {
33
34 /// State holds the current transform state
35 struct Robustness::State {
36 /// The clone context
37 CloneContext& ctx;
38
39 /// Set of storage classes to not apply the transform to
40 std::unordered_set<ast::StorageClass> omitted_classes;
41
42 /// Applies the transformation state to `ctx`.
Transformtint::transform::Robustness::State43 void Transform() {
44 ctx.ReplaceAll([&](const ast::IndexAccessorExpression* expr) {
45 return Transform(expr);
46 });
47 ctx.ReplaceAll(
48 [&](const ast::CallExpression* expr) { return Transform(expr); });
49 }
50
51 /// Apply bounds clamping to array, vector and matrix indexing
52 /// @param expr the array, vector or matrix index expression
53 /// @return the clamped replacement expression, or nullptr if `expr` should be
54 /// cloned without changes.
Transformtint::transform::Robustness::State55 const ast::IndexAccessorExpression* Transform(
56 const ast::IndexAccessorExpression* expr) {
57 auto* ret_type = ctx.src->Sem().Get(expr->object)->Type();
58
59 auto* ref = ret_type->As<sem::Reference>();
60 if (ref && omitted_classes.count(ref->StorageClass()) != 0) {
61 return nullptr;
62 }
63
64 auto* ret_unwrapped = ret_type->UnwrapRef();
65
66 ProgramBuilder& b = *ctx.dst;
67 using u32 = ProgramBuilder::u32;
68
69 struct Value {
70 const ast::Expression* expr = nullptr; // If null, then is a constant
71 union {
72 uint32_t u32 = 0; // use if is_signed == false
73 int32_t i32; // use if is_signed == true
74 };
75 bool is_signed = false;
76 };
77
78 Value size; // size of the array, vector or matrix
79 size.is_signed = false; // size is always unsigned
80 if (auto* vec = ret_unwrapped->As<sem::Vector>()) {
81 size.u32 = vec->Width();
82
83 } else if (auto* arr = ret_unwrapped->As<sem::Array>()) {
84 size.u32 = arr->Count();
85 } else if (auto* mat = ret_unwrapped->As<sem::Matrix>()) {
86 // The row accessor would have been an embedded index accessor and already
87 // handled, so we just need to do columns here.
88 size.u32 = mat->columns();
89 } else {
90 return nullptr;
91 }
92
93 if (size.u32 == 0) {
94 if (!ret_unwrapped->Is<sem::Array>()) {
95 b.Diagnostics().add_error(diag::System::Transform,
96 "invalid 0 sized non-array", expr->source);
97 return nullptr;
98 }
99 // Runtime sized array
100 auto* arr = ctx.Clone(expr->object);
101 size.expr = b.Call("arrayLength", b.AddressOf(arr));
102 }
103
104 // Calculate the maximum possible index value (size-1u)
105 // Size must be positive (non-zero), so we can safely subtract 1 here
106 // without underflow.
107 Value limit;
108 limit.is_signed = false; // Like size, limit is always unsigned.
109 if (size.expr) {
110 // Dynamic size
111 limit.expr = b.Sub(size.expr, 1u);
112 } else {
113 // Constant size
114 limit.u32 = size.u32 - 1u;
115 }
116
117 Value idx; // index value
118
119 auto* idx_sem = ctx.src->Sem().Get(expr->index);
120 auto* idx_ty = idx_sem->Type()->UnwrapRef();
121 if (!idx_ty->IsAnyOf<sem::I32, sem::U32>()) {
122 TINT_ICE(Transform, b.Diagnostics())
123 << "index must be u32 or i32, got " << idx_sem->Type()->type_name();
124 return nullptr;
125 }
126
127 if (auto idx_constant = idx_sem->ConstantValue()) {
128 // Constant value index
129 if (idx_constant.Type()->Is<sem::I32>()) {
130 idx.i32 = idx_constant.Elements()[0].i32;
131 idx.is_signed = true;
132 } else if (idx_constant.Type()->Is<sem::U32>()) {
133 idx.u32 = idx_constant.Elements()[0].u32;
134 idx.is_signed = false;
135 } else {
136 b.Diagnostics().add_error(diag::System::Transform,
137 "unsupported constant value for accessor: " +
138 idx_constant.Type()->type_name(),
139 expr->source);
140 return nullptr;
141 }
142 } else {
143 // Dynamic value index
144 idx.expr = ctx.Clone(expr->index);
145 idx.is_signed = idx_ty->Is<sem::I32>();
146 }
147
148 // Clamp the index so that it cannot exceed limit.
149 if (idx.expr || limit.expr) {
150 // One of, or both of idx and limit are non-constant.
151
152 // If the index is signed, cast it to a u32 (with clamping if constant).
153 if (idx.is_signed) {
154 if (idx.expr) {
155 // We don't use a max(idx, 0) here, as that incurs a runtime
156 // performance cost, and if the unsigned value will be clamped by
157 // limit, resulting in a value between [0..limit)
158 idx.expr = b.Construct<u32>(idx.expr);
159 idx.is_signed = false;
160 } else {
161 idx.u32 = static_cast<uint32_t>(std::max(idx.i32, 0));
162 idx.is_signed = false;
163 }
164 }
165
166 // Convert idx and limit to expressions, so we can emit `min(idx, limit)`.
167 if (!idx.expr) {
168 idx.expr = b.Expr(idx.u32);
169 }
170 if (!limit.expr) {
171 limit.expr = b.Expr(limit.u32);
172 }
173
174 // Perform the clamp with `min(idx, limit)`
175 idx.expr = b.Call("min", idx.expr, limit.expr);
176 } else {
177 // Both idx and max are constant.
178 if (idx.is_signed) {
179 // The index is signed. Calculate limit as signed.
180 int32_t signed_limit = static_cast<int32_t>(
181 std::min<uint32_t>(limit.u32, std::numeric_limits<int32_t>::max()));
182 idx.i32 = std::max(idx.i32, 0);
183 idx.i32 = std::min(idx.i32, signed_limit);
184 } else {
185 // The index is unsigned.
186 idx.u32 = std::min(idx.u32, limit.u32);
187 }
188 }
189
190 // Convert idx to an expression, so we can emit the new accessor.
191 if (!idx.expr) {
192 idx.expr = idx.is_signed
193 ? static_cast<const ast::Expression*>(b.Expr(idx.i32))
194 : static_cast<const ast::Expression*>(b.Expr(idx.u32));
195 }
196
197 // Clone arguments outside of create() call to have deterministic ordering
198 auto src = ctx.Clone(expr->source);
199 auto* obj = ctx.Clone(expr->object);
200 return b.IndexAccessor(src, obj, idx.expr);
201 }
202
203 /// @param type intrinsic type
204 /// @returns true if the given intrinsic is a texture function that requires
205 /// argument clamping,
TextureIntrinsicNeedsClampingtint::transform::Robustness::State206 bool TextureIntrinsicNeedsClamping(sem::IntrinsicType type) {
207 return type == sem::IntrinsicType::kTextureLoad ||
208 type == sem::IntrinsicType::kTextureStore;
209 }
210
211 /// Apply bounds clamping to the coordinates, array index and level arguments
212 /// of the `textureLoad()` and `textureStore()` intrinsics.
213 /// @param expr the intrinsic call expression
214 /// @return the clamped replacement call expression, or nullptr if `expr`
215 /// should be cloned without changes.
Transformtint::transform::Robustness::State216 const ast::CallExpression* Transform(const ast::CallExpression* expr) {
217 auto* call = ctx.src->Sem().Get(expr);
218 auto* call_target = call->Target();
219 auto* intrinsic = call_target->As<sem::Intrinsic>();
220 if (!intrinsic || !TextureIntrinsicNeedsClamping(intrinsic->Type())) {
221 return nullptr; // No transform, just clone.
222 }
223
224 ProgramBuilder& b = *ctx.dst;
225
226 // Indices of the mandatory texture and coords parameters, and the optional
227 // array and level parameters.
228 auto& signature = intrinsic->Signature();
229 auto texture_idx = signature.IndexOf(sem::ParameterUsage::kTexture);
230 auto coords_idx = signature.IndexOf(sem::ParameterUsage::kCoords);
231 auto array_idx = signature.IndexOf(sem::ParameterUsage::kArrayIndex);
232 auto level_idx = signature.IndexOf(sem::ParameterUsage::kLevel);
233
234 auto* texture_arg = expr->args[texture_idx];
235 auto* coords_arg = expr->args[coords_idx];
236 auto* coords_ty = intrinsic->Parameters()[coords_idx]->Type();
237
238 // If the level is provided, then we need to clamp this. As the level is
239 // used by textureDimensions() and the texture[Load|Store]() calls, we need
240 // to clamp both usages.
241 // TODO(bclayton): We probably want to place this into a let so that the
242 // calculation can be reused. This is fiddly to get right.
243 std::function<const ast::Expression*()> level_arg;
244 if (level_idx >= 0) {
245 level_arg = [&] {
246 auto* arg = expr->args[level_idx];
247 auto* num_levels = b.Call("textureNumLevels", ctx.Clone(texture_arg));
248 auto* zero = b.Expr(0);
249 auto* max = ctx.dst->Sub(num_levels, 1);
250 auto* clamped = b.Call("clamp", ctx.Clone(arg), zero, max);
251 return clamped;
252 };
253 }
254
255 // Clamp the coordinates argument
256 {
257 auto* texture_dims =
258 level_arg
259 ? b.Call("textureDimensions", ctx.Clone(texture_arg), level_arg())
260 : b.Call("textureDimensions", ctx.Clone(texture_arg));
261 auto* zero = b.Construct(CreateASTTypeFor(ctx, coords_ty));
262 auto* max = ctx.dst->Sub(
263 texture_dims, b.Construct(CreateASTTypeFor(ctx, coords_ty), 1));
264 auto* clamped_coords = b.Call("clamp", ctx.Clone(coords_arg), zero, max);
265 ctx.Replace(coords_arg, clamped_coords);
266 }
267
268 // Clamp the array_index argument, if provided
269 if (array_idx >= 0) {
270 auto* arg = expr->args[array_idx];
271 auto* num_layers = b.Call("textureNumLayers", ctx.Clone(texture_arg));
272 auto* zero = b.Expr(0);
273 auto* max = ctx.dst->Sub(num_layers, 1);
274 auto* clamped = b.Call("clamp", ctx.Clone(arg), zero, max);
275 ctx.Replace(arg, clamped);
276 }
277
278 // Clamp the level argument, if provided
279 if (level_idx >= 0) {
280 auto* arg = expr->args[level_idx];
281 ctx.Replace(arg, level_arg ? level_arg() : ctx.dst->Expr(0));
282 }
283
284 return nullptr; // Clone, which will use the argument replacements above.
285 }
286 };
287
288 Robustness::Config::Config() = default;
289 Robustness::Config::Config(const Config&) = default;
290 Robustness::Config::~Config() = default;
291 Robustness::Config& Robustness::Config::operator=(const Config&) = default;
292
293 Robustness::Robustness() = default;
294 Robustness::~Robustness() = default;
295
Run(CloneContext & ctx,const DataMap & inputs,DataMap &)296 void Robustness::Run(CloneContext& ctx, const DataMap& inputs, DataMap&) {
297 Config cfg;
298 if (auto* cfg_data = inputs.Get<Config>()) {
299 cfg = *cfg_data;
300 }
301
302 std::unordered_set<ast::StorageClass> omitted_classes;
303 for (auto sc : cfg.omitted_classes) {
304 switch (sc) {
305 case StorageClass::kUniform:
306 omitted_classes.insert(ast::StorageClass::kUniform);
307 break;
308 case StorageClass::kStorage:
309 omitted_classes.insert(ast::StorageClass::kStorage);
310 break;
311 }
312 }
313
314 State state{ctx, std::move(omitted_classes)};
315
316 state.Transform();
317 ctx.Clone();
318 }
319
320 } // namespace transform
321 } // namespace tint
322