1 // Copyright (c) 2018 Google LLC
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 "source/reduce/reducer.h"
16 #include "source/reduce/reduction_opportunity.h"
17 #include "source/reduce/remove_instruction_reduction_opportunity.h"
18 #include "test/reduce/reduce_test_util.h"
19
20 namespace spvtools {
21 namespace reduce {
22 namespace {
23
24 using opt::Function;
25 using opt::IRContext;
26 using opt::Instruction;
27
28 // A reduction opportunity finder that finds opportunities to remove global
29 // values regardless of whether they are referenced. This is very likely to make
30 // the resulting module invalid. We use this to test the reducer's behavior in
31 // the scenario where a bad reduction pass leads to an invalid module.
32 class BlindlyRemoveGlobalValuesReductionOpportunityFinder
33 : public ReductionOpportunityFinder {
34 public:
35 BlindlyRemoveGlobalValuesReductionOpportunityFinder() = default;
36
37 ~BlindlyRemoveGlobalValuesReductionOpportunityFinder() override = default;
38
39 // The name of this pass.
GetName() const40 std::string GetName() const final { return "BlindlyRemoveGlobalValuesPass"; }
41
42 // Finds opportunities to remove all global values. Assuming they are all
43 // referenced (directly or indirectly) from elsewhere in the module, each such
44 // opportunity will make the module invalid.
GetAvailableOpportunities(IRContext * context,uint32_t) const45 std::vector<std::unique_ptr<ReductionOpportunity>> GetAvailableOpportunities(
46 IRContext* context, uint32_t /*unused*/) const final {
47 std::vector<std::unique_ptr<ReductionOpportunity>> result;
48 for (auto& inst : context->module()->types_values()) {
49 if (inst.HasResultId()) {
50 result.push_back(
51 MakeUnique<RemoveInstructionReductionOpportunity>(&inst));
52 }
53 }
54 return result;
55 }
56 };
57
58 // A reduction opportunity that exists at the start of every function whose
59 // first instruction is an OpVariable instruction. When applied, the OpVariable
60 // instruction is duplicated (with a fresh result id). This allows each
61 // reduction step to increase the number of variables to check if the validator
62 // limits are enforced.
63 class OpVariableDuplicatorReductionOpportunity : public ReductionOpportunity {
64 public:
OpVariableDuplicatorReductionOpportunity(Function * function)65 OpVariableDuplicatorReductionOpportunity(Function* function)
66 : function_(function) {}
67
PreconditionHolds()68 bool PreconditionHolds() override {
69 Instruction* first_instruction = &*function_->begin()[0].begin();
70 return first_instruction->opcode() == SpvOpVariable;
71 }
72
73 protected:
Apply()74 void Apply() override {
75 // Duplicate the first OpVariable instruction.
76
77 Instruction* first_instruction = &*function_->begin()[0].begin();
78 assert(first_instruction->opcode() == SpvOpVariable &&
79 "Expected first instruction to be OpVariable");
80 IRContext* context = first_instruction->context();
81 Instruction* cloned_instruction = first_instruction->Clone(context);
82 cloned_instruction->SetResultId(context->TakeNextId());
83 cloned_instruction->InsertBefore(first_instruction);
84 }
85
86 private:
87 Function* function_;
88 };
89
90 // A reduction opportunity finder that finds
91 // OpVariableDuplicatorReductionOpportunity.
92 class OpVariableDuplicatorReductionOpportunityFinder
93 : public ReductionOpportunityFinder {
94 public:
95 OpVariableDuplicatorReductionOpportunityFinder() = default;
96
97 ~OpVariableDuplicatorReductionOpportunityFinder() override = default;
98
GetName() const99 std::string GetName() const final {
100 return "LocalVariableAdderReductionOpportunityFinder";
101 }
102
GetAvailableOpportunities(IRContext * context,uint32_t) const103 std::vector<std::unique_ptr<ReductionOpportunity>> GetAvailableOpportunities(
104 IRContext* context, uint32_t /*unused*/) const final {
105 std::vector<std::unique_ptr<ReductionOpportunity>> result;
106 for (auto& function : *context->module()) {
107 Instruction* first_instruction = &*function.begin()[0].begin();
108 if (first_instruction->opcode() == SpvOpVariable) {
109 result.push_back(
110 MakeUnique<OpVariableDuplicatorReductionOpportunity>(&function));
111 }
112 }
113 return result;
114 }
115 };
116
TEST(ValidationDuringReductionTest,CheckInvalidPassMakesNoProgress)117 TEST(ValidationDuringReductionTest, CheckInvalidPassMakesNoProgress) {
118 // A module whose global values are all referenced, so that any application of
119 // MakeModuleInvalidPass will make the module invalid. Check that the reducer
120 // makes no progress, as every step will be invalid and treated as
121 // uninteresting.
122 std::string original = R"(
123 OpCapability Shader
124 %1 = OpExtInstImport "GLSL.std.450"
125 OpMemoryModel Logical GLSL450
126 OpEntryPoint Fragment %4 "main" %60
127 OpExecutionMode %4 OriginUpperLeft
128 OpSource ESSL 310
129 OpName %4 "main"
130 OpName %16 "buf2"
131 OpMemberName %16 0 "i"
132 OpName %18 ""
133 OpName %25 "buf1"
134 OpMemberName %25 0 "f"
135 OpName %27 ""
136 OpName %60 "_GLF_color"
137 OpMemberDecorate %16 0 Offset 0
138 OpDecorate %16 Block
139 OpDecorate %18 DescriptorSet 0
140 OpDecorate %18 Binding 2
141 OpMemberDecorate %25 0 Offset 0
142 OpDecorate %25 Block
143 OpDecorate %27 DescriptorSet 0
144 OpDecorate %27 Binding 1
145 OpDecorate %60 Location 0
146 %2 = OpTypeVoid
147 %3 = OpTypeFunction %2
148 %6 = OpTypeInt 32 1
149 %9 = OpConstant %6 0
150 %16 = OpTypeStruct %6
151 %17 = OpTypePointer Uniform %16
152 %18 = OpVariable %17 Uniform
153 %19 = OpTypePointer Uniform %6
154 %22 = OpTypeBool
155 %24 = OpTypeFloat 32
156 %25 = OpTypeStruct %24
157 %26 = OpTypePointer Uniform %25
158 %27 = OpVariable %26 Uniform
159 %28 = OpTypePointer Uniform %24
160 %31 = OpConstant %24 2
161 %56 = OpConstant %6 1
162 %58 = OpTypeVector %24 4
163 %59 = OpTypePointer Output %58
164 %60 = OpVariable %59 Output
165 %72 = OpUndef %24
166 %74 = OpUndef %6
167 %4 = OpFunction %2 None %3
168 %5 = OpLabel
169 OpBranch %10
170 %10 = OpLabel
171 %73 = OpPhi %6 %74 %5 %77 %34
172 %71 = OpPhi %24 %72 %5 %76 %34
173 %70 = OpPhi %6 %9 %5 %57 %34
174 %20 = OpAccessChain %19 %18 %9
175 %21 = OpLoad %6 %20
176 %23 = OpSLessThan %22 %70 %21
177 OpLoopMerge %12 %34 None
178 OpBranchConditional %23 %11 %12
179 %11 = OpLabel
180 %29 = OpAccessChain %28 %27 %9
181 %30 = OpLoad %24 %29
182 %32 = OpFOrdGreaterThan %22 %30 %31
183 OpSelectionMerge %90 None
184 OpBranchConditional %32 %33 %46
185 %33 = OpLabel
186 %40 = OpFAdd %24 %71 %30
187 %45 = OpISub %6 %73 %21
188 OpBranch %90
189 %46 = OpLabel
190 %50 = OpFMul %24 %71 %30
191 %54 = OpSDiv %6 %73 %21
192 OpBranch %90
193 %90 = OpLabel
194 %77 = OpPhi %6 %45 %33 %54 %46
195 %76 = OpPhi %24 %40 %33 %50 %46
196 OpBranch %34
197 %34 = OpLabel
198 %57 = OpIAdd %6 %70 %56
199 OpBranch %10
200 %12 = OpLabel
201 %61 = OpAccessChain %28 %27 %9
202 %62 = OpLoad %24 %61
203 %66 = OpConvertSToF %24 %21
204 %68 = OpConvertSToF %24 %73
205 %69 = OpCompositeConstruct %58 %62 %71 %66 %68
206 OpStore %60 %69
207 OpReturn
208 OpFunctionEnd
209 )";
210
211 spv_target_env env = SPV_ENV_UNIVERSAL_1_3;
212 Reducer reducer(env);
213 reducer.SetMessageConsumer(NopDiagnostic);
214
215 // Say that every module is interesting.
216 reducer.SetInterestingnessFunction(
217 [](const std::vector<uint32_t>&, uint32_t) -> bool { return true; });
218
219 reducer.AddReductionPass(
220 MakeUnique<BlindlyRemoveGlobalValuesReductionOpportunityFinder>());
221
222 std::vector<uint32_t> binary_in;
223 SpirvTools t(env);
224
225 ASSERT_TRUE(t.Assemble(original, &binary_in, kReduceAssembleOption));
226 std::vector<uint32_t> binary_out;
227 spvtools::ReducerOptions reducer_options;
228 reducer_options.set_step_limit(500);
229 // Don't fail on a validation error; just treat it as uninteresting.
230 reducer_options.set_fail_on_validation_error(false);
231 spvtools::ValidatorOptions validator_options;
232
233 Reducer::ReductionResultStatus status = reducer.Run(
234 std::move(binary_in), &binary_out, reducer_options, validator_options);
235
236 ASSERT_EQ(status, Reducer::ReductionResultStatus::kComplete);
237
238 // The reducer should have no impact.
239 CheckEqual(env, original, binary_out);
240 }
241
TEST(ValidationDuringReductionTest,CheckNotAlwaysInvalidCanMakeProgress)242 TEST(ValidationDuringReductionTest, CheckNotAlwaysInvalidCanMakeProgress) {
243 // A module with just one unreferenced global value. All but one application
244 // of MakeModuleInvalidPass will make the module invalid.
245 std::string original = R"(
246 OpCapability Shader
247 %1 = OpExtInstImport "GLSL.std.450"
248 OpMemoryModel Logical GLSL450
249 OpEntryPoint Fragment %4 "main" %60
250 OpExecutionMode %4 OriginUpperLeft
251 OpSource ESSL 310
252 OpName %4 "main"
253 OpName %16 "buf2"
254 OpMemberName %16 0 "i"
255 OpName %18 ""
256 OpName %25 "buf1"
257 OpMemberName %25 0 "f"
258 OpName %27 ""
259 OpName %60 "_GLF_color"
260 OpMemberDecorate %16 0 Offset 0
261 OpDecorate %16 Block
262 OpDecorate %18 DescriptorSet 0
263 OpDecorate %18 Binding 2
264 OpMemberDecorate %25 0 Offset 0
265 OpDecorate %25 Block
266 OpDecorate %27 DescriptorSet 0
267 OpDecorate %27 Binding 1
268 OpDecorate %60 Location 0
269 %2 = OpTypeVoid
270 %3 = OpTypeFunction %2
271 %6 = OpTypeInt 32 1
272 %9 = OpConstant %6 0
273 %16 = OpTypeStruct %6
274 %17 = OpTypePointer Uniform %16
275 %18 = OpVariable %17 Uniform
276 %19 = OpTypePointer Uniform %6
277 %22 = OpTypeBool
278 %24 = OpTypeFloat 32
279 %25 = OpTypeStruct %24
280 %26 = OpTypePointer Uniform %25
281 %27 = OpVariable %26 Uniform
282 %28 = OpTypePointer Uniform %24
283 %31 = OpConstant %24 2
284 %56 = OpConstant %6 1
285 %1000 = OpConstant %6 1000 ; It should be possible to remove this instruction without making the module invalid.
286 %58 = OpTypeVector %24 4
287 %59 = OpTypePointer Output %58
288 %60 = OpVariable %59 Output
289 %72 = OpUndef %24
290 %74 = OpUndef %6
291 %4 = OpFunction %2 None %3
292 %5 = OpLabel
293 OpBranch %10
294 %10 = OpLabel
295 %73 = OpPhi %6 %74 %5 %77 %34
296 %71 = OpPhi %24 %72 %5 %76 %34
297 %70 = OpPhi %6 %9 %5 %57 %34
298 %20 = OpAccessChain %19 %18 %9
299 %21 = OpLoad %6 %20
300 %23 = OpSLessThan %22 %70 %21
301 OpLoopMerge %12 %34 None
302 OpBranchConditional %23 %11 %12
303 %11 = OpLabel
304 %29 = OpAccessChain %28 %27 %9
305 %30 = OpLoad %24 %29
306 %32 = OpFOrdGreaterThan %22 %30 %31
307 OpSelectionMerge %90 None
308 OpBranchConditional %32 %33 %46
309 %33 = OpLabel
310 %40 = OpFAdd %24 %71 %30
311 %45 = OpISub %6 %73 %21
312 OpBranch %90
313 %46 = OpLabel
314 %50 = OpFMul %24 %71 %30
315 %54 = OpSDiv %6 %73 %21
316 OpBranch %90
317 %90 = OpLabel
318 %77 = OpPhi %6 %45 %33 %54 %46
319 %76 = OpPhi %24 %40 %33 %50 %46
320 OpBranch %34
321 %34 = OpLabel
322 %57 = OpIAdd %6 %70 %56
323 OpBranch %10
324 %12 = OpLabel
325 %61 = OpAccessChain %28 %27 %9
326 %62 = OpLoad %24 %61
327 %66 = OpConvertSToF %24 %21
328 %68 = OpConvertSToF %24 %73
329 %69 = OpCompositeConstruct %58 %62 %71 %66 %68
330 OpStore %60 %69
331 OpReturn
332 OpFunctionEnd
333 )";
334
335 // This is the same as the original, except that the constant declaration of
336 // 1000 is gone.
337 std::string expected = R"(
338 OpCapability Shader
339 %1 = OpExtInstImport "GLSL.std.450"
340 OpMemoryModel Logical GLSL450
341 OpEntryPoint Fragment %4 "main" %60
342 OpExecutionMode %4 OriginUpperLeft
343 OpSource ESSL 310
344 OpName %4 "main"
345 OpName %16 "buf2"
346 OpMemberName %16 0 "i"
347 OpName %18 ""
348 OpName %25 "buf1"
349 OpMemberName %25 0 "f"
350 OpName %27 ""
351 OpName %60 "_GLF_color"
352 OpMemberDecorate %16 0 Offset 0
353 OpDecorate %16 Block
354 OpDecorate %18 DescriptorSet 0
355 OpDecorate %18 Binding 2
356 OpMemberDecorate %25 0 Offset 0
357 OpDecorate %25 Block
358 OpDecorate %27 DescriptorSet 0
359 OpDecorate %27 Binding 1
360 OpDecorate %60 Location 0
361 %2 = OpTypeVoid
362 %3 = OpTypeFunction %2
363 %6 = OpTypeInt 32 1
364 %9 = OpConstant %6 0
365 %16 = OpTypeStruct %6
366 %17 = OpTypePointer Uniform %16
367 %18 = OpVariable %17 Uniform
368 %19 = OpTypePointer Uniform %6
369 %22 = OpTypeBool
370 %24 = OpTypeFloat 32
371 %25 = OpTypeStruct %24
372 %26 = OpTypePointer Uniform %25
373 %27 = OpVariable %26 Uniform
374 %28 = OpTypePointer Uniform %24
375 %31 = OpConstant %24 2
376 %56 = OpConstant %6 1
377 %58 = OpTypeVector %24 4
378 %59 = OpTypePointer Output %58
379 %60 = OpVariable %59 Output
380 %72 = OpUndef %24
381 %74 = OpUndef %6
382 %4 = OpFunction %2 None %3
383 %5 = OpLabel
384 OpBranch %10
385 %10 = OpLabel
386 %73 = OpPhi %6 %74 %5 %77 %34
387 %71 = OpPhi %24 %72 %5 %76 %34
388 %70 = OpPhi %6 %9 %5 %57 %34
389 %20 = OpAccessChain %19 %18 %9
390 %21 = OpLoad %6 %20
391 %23 = OpSLessThan %22 %70 %21
392 OpLoopMerge %12 %34 None
393 OpBranchConditional %23 %11 %12
394 %11 = OpLabel
395 %29 = OpAccessChain %28 %27 %9
396 %30 = OpLoad %24 %29
397 %32 = OpFOrdGreaterThan %22 %30 %31
398 OpSelectionMerge %90 None
399 OpBranchConditional %32 %33 %46
400 %33 = OpLabel
401 %40 = OpFAdd %24 %71 %30
402 %45 = OpISub %6 %73 %21
403 OpBranch %90
404 %46 = OpLabel
405 %50 = OpFMul %24 %71 %30
406 %54 = OpSDiv %6 %73 %21
407 OpBranch %90
408 %90 = OpLabel
409 %77 = OpPhi %6 %45 %33 %54 %46
410 %76 = OpPhi %24 %40 %33 %50 %46
411 OpBranch %34
412 %34 = OpLabel
413 %57 = OpIAdd %6 %70 %56
414 OpBranch %10
415 %12 = OpLabel
416 %61 = OpAccessChain %28 %27 %9
417 %62 = OpLoad %24 %61
418 %66 = OpConvertSToF %24 %21
419 %68 = OpConvertSToF %24 %73
420 %69 = OpCompositeConstruct %58 %62 %71 %66 %68
421 OpStore %60 %69
422 OpReturn
423 OpFunctionEnd
424 )";
425
426 spv_target_env env = SPV_ENV_UNIVERSAL_1_3;
427 Reducer reducer(env);
428 reducer.SetMessageConsumer(NopDiagnostic);
429
430 // Say that every module is interesting.
431 reducer.SetInterestingnessFunction(
432 [](const std::vector<uint32_t>&, uint32_t) -> bool { return true; });
433
434 reducer.AddReductionPass(
435 MakeUnique<BlindlyRemoveGlobalValuesReductionOpportunityFinder>());
436
437 std::vector<uint32_t> binary_in;
438 SpirvTools t(env);
439
440 ASSERT_TRUE(t.Assemble(original, &binary_in, kReduceAssembleOption));
441 std::vector<uint32_t> binary_out;
442 spvtools::ReducerOptions reducer_options;
443 reducer_options.set_step_limit(500);
444 // Don't fail on a validation error; just treat it as uninteresting.
445 reducer_options.set_fail_on_validation_error(false);
446 spvtools::ValidatorOptions validator_options;
447
448 Reducer::ReductionResultStatus status = reducer.Run(
449 std::move(binary_in), &binary_out, reducer_options, validator_options);
450
451 ASSERT_EQ(status, Reducer::ReductionResultStatus::kComplete);
452
453 CheckEqual(env, expected, binary_out);
454 }
455
456 // Sets up a Reducer for use in the CheckValidationOptions test; avoids
457 // repetition.
SetupReducerForCheckValidationOptions(Reducer * reducer)458 void SetupReducerForCheckValidationOptions(Reducer* reducer) {
459 reducer->SetMessageConsumer(NopDiagnostic);
460
461 // Say that every module is interesting.
462 reducer->SetInterestingnessFunction(
463 [](const std::vector<uint32_t>&, uint32_t) -> bool { return true; });
464
465 // Each "reduction" step will duplicate the first OpVariable instruction in
466 // the function.
467 reducer->AddReductionPass(
468 MakeUnique<OpVariableDuplicatorReductionOpportunityFinder>());
469 }
470
TEST(ValidationDuringReductionTest,CheckValidationOptions)471 TEST(ValidationDuringReductionTest, CheckValidationOptions) {
472 // A module that only validates when the "skip-block-layout" validator option
473 // is used. Also, the entry point's first instruction creates a local
474 // variable; this instruction will be duplicated on each reduction step.
475 std::string original = R"(
476 OpCapability Shader
477 %1 = OpExtInstImport "GLSL.std.450"
478 OpMemoryModel Logical GLSL450
479 OpEntryPoint Vertex %2 "Main" %3
480 OpSource HLSL 600
481 OpDecorate %3 BuiltIn Position
482 OpDecorate %4 DescriptorSet 0
483 OpDecorate %4 Binding 99
484 OpDecorate %5 ArrayStride 16
485 OpMemberDecorate %6 0 Offset 0
486 OpMemberDecorate %6 1 Offset 32
487 OpMemberDecorate %6 1 MatrixStride 16
488 OpMemberDecorate %6 1 ColMajor
489 OpMemberDecorate %6 2 Offset 96
490 OpMemberDecorate %6 3 Offset 100
491 OpMemberDecorate %6 4 Offset 112
492 OpMemberDecorate %6 4 MatrixStride 16
493 OpMemberDecorate %6 4 ColMajor
494 OpMemberDecorate %6 5 Offset 176
495 OpDecorate %6 Block
496 %7 = OpTypeFloat 32
497 %8 = OpTypeVector %7 4
498 %9 = OpTypeMatrix %8 4
499 %10 = OpTypeVector %7 2
500 %11 = OpTypeInt 32 1
501 %12 = OpTypeInt 32 0
502 %13 = OpConstant %12 2
503 %14 = OpConstant %11 1
504 %15 = OpConstant %11 5
505 %5 = OpTypeArray %8 %13
506 %6 = OpTypeStruct %5 %9 %12 %10 %9 %7
507 %16 = OpTypePointer Uniform %6
508 %17 = OpTypePointer Output %8
509 %18 = OpTypeVoid
510 %19 = OpTypeFunction %18
511 %20 = OpTypePointer Uniform %7
512 %4 = OpVariable %16 Uniform
513 %3 = OpVariable %17 Output
514 %21 = OpTypePointer Function %11
515 %2 = OpFunction %18 None %19
516 %22 = OpLabel
517 %23 = OpVariable %21 Function
518 %24 = OpAccessChain %20 %4 %15
519 %25 = OpLoad %7 %24
520 %26 = OpCompositeConstruct %8 %25 %25 %25 %25
521 OpStore %3 %26
522 OpReturn
523 OpFunctionEnd
524 )";
525
526 spv_target_env env = SPV_ENV_UNIVERSAL_1_3;
527 std::vector<uint32_t> binary_in;
528 SpirvTools t(env);
529
530 ASSERT_TRUE(t.Assemble(original, &binary_in, kReduceAssembleOption));
531 std::vector<uint32_t> binary_out;
532 spvtools::ReducerOptions reducer_options;
533 spvtools::ValidatorOptions validator_options;
534
535 reducer_options.set_step_limit(3);
536 reducer_options.set_fail_on_validation_error(true);
537
538 // Reduction should fail because the initial state is invalid without the
539 // "skip-block-layout" validator option. Note that the interestingness test
540 // always returns true.
541 {
542 Reducer reducer(env);
543 SetupReducerForCheckValidationOptions(&reducer);
544
545 Reducer::ReductionResultStatus status =
546 reducer.Run(std::vector<uint32_t>(binary_in), &binary_out,
547 reducer_options, validator_options);
548
549 ASSERT_EQ(status, Reducer::ReductionResultStatus::kInitialStateInvalid);
550 }
551
552 // Try again with validator option.
553 validator_options.SetSkipBlockLayout(true);
554
555 // Reduction should hit step limit; module is seen as valid, interestingness
556 // test always succeeds, and the finder yields infinite opportunities.
557 {
558 Reducer reducer(env);
559 SetupReducerForCheckValidationOptions(&reducer);
560
561 Reducer::ReductionResultStatus status =
562 reducer.Run(std::vector<uint32_t>(binary_in), &binary_out,
563 reducer_options, validator_options);
564
565 ASSERT_EQ(status, Reducer::ReductionResultStatus::kReachedStepLimit);
566 }
567
568 // Now set a limit on the number of local variables.
569 validator_options.SetUniversalLimit(spv_validator_limit_max_local_variables,
570 2);
571
572 // Reduction should now fail due to reaching an invalid state; after one step,
573 // a local variable is added and the module becomes "invalid" given the
574 // validator limits.
575 {
576 Reducer reducer(env);
577 SetupReducerForCheckValidationOptions(&reducer);
578
579 Reducer::ReductionResultStatus status =
580 reducer.Run(std::vector<uint32_t>(binary_in), &binary_out,
581 reducer_options, validator_options);
582
583 ASSERT_EQ(status, Reducer::ReductionResultStatus::kStateInvalid);
584 }
585 }
586
587 } // namespace
588 } // namespace reduce
589 } // namespace spvtools
590