From fd05605bef1c63c6f249c19ed90c7ba24f8a8150 Mon Sep 17 00:00:00 2001 From: Stefano Milizia Date: Thu, 3 Sep 2020 11:19:02 +0200 Subject: [PATCH] spirv-fuzz: Transformation to convert OpSelect to conditional branch (#3681) This transformation takes an OpSelect instruction and replaces it with a conditional branch, selecting the correct value using an OpPhi instruction. Fixes part of the issue #3544. --- source/fuzz/CMakeLists.txt | 4 + source/fuzz/fuzzer.cpp | 4 + source/fuzz/fuzzer_context.cpp | 12 + source/fuzz/fuzzer_context.h | 12 + ...ce_opselects_with_conditional_branches.cpp | 162 ++++++++++ ...lace_opselects_with_conditional_branches.h | 53 ++++ source/fuzz/fuzzer_util.cpp | 33 +++ source/fuzz/fuzzer_util.h | 5 + source/fuzz/protobufs/spvtoolsfuzz.proto | 34 +++ source/fuzz/transformation.cpp | 5 + ...place_opselect_with_conditional_branch.cpp | 204 +++++++++++++ ...replace_opselect_with_conditional_branch.h | 61 ++++ source/fuzz/transformation_split_block.cpp | 24 +- test/fuzz/CMakeLists.txt | 1 + ..._opselect_with_conditional_branch_test.cpp | 278 ++++++++++++++++++ 15 files changed, 871 insertions(+), 21 deletions(-) create mode 100644 source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.cpp create mode 100644 source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h create mode 100644 source/fuzz/transformation_replace_opselect_with_conditional_branch.cpp create mode 100644 source/fuzz/transformation_replace_opselect_with_conditional_branch.h create mode 100644 test/fuzz/transformation_replace_opselect_with_conditional_branch_test.cpp diff --git a/source/fuzz/CMakeLists.txt b/source/fuzz/CMakeLists.txt index 288ea25c1..523f57867 100644 --- a/source/fuzz/CMakeLists.txt +++ b/source/fuzz/CMakeLists.txt @@ -95,6 +95,7 @@ if(SPIRV_BUILD_FUZZER) fuzzer_pass_replace_linear_algebra_instructions.h fuzzer_pass_replace_loads_stores_with_copy_memories.h fuzzer_pass_replace_opphi_ids_from_dead_predecessors.h + fuzzer_pass_replace_opselects_with_conditional_branches.h fuzzer_pass_replace_parameter_with_global.h fuzzer_pass_replace_params_with_struct.h fuzzer_pass_split_blocks.h @@ -174,6 +175,7 @@ if(SPIRV_BUILD_FUZZER) transformation_replace_linear_algebra_instruction.h transformation_replace_load_store_with_copy_memory.h transformation_replace_opphi_id_from_dead_predecessor.h + transformation_replace_opselect_with_conditional_branch.h transformation_replace_parameter_with_global.h transformation_replace_params_with_struct.h transformation_set_function_control.h @@ -254,6 +256,7 @@ if(SPIRV_BUILD_FUZZER) fuzzer_pass_replace_linear_algebra_instructions.cpp fuzzer_pass_replace_loads_stores_with_copy_memories.cpp fuzzer_pass_replace_opphi_ids_from_dead_predecessors.cpp + fuzzer_pass_replace_opselects_with_conditional_branches.cpp fuzzer_pass_replace_parameter_with_global.cpp fuzzer_pass_replace_params_with_struct.cpp fuzzer_pass_split_blocks.cpp @@ -306,6 +309,7 @@ if(SPIRV_BUILD_FUZZER) transformation_composite_insert.cpp transformation_compute_data_synonym_fact_closure.cpp transformation_context.cpp + transformation_replace_opselect_with_conditional_branch.cpp transformation_equation_instruction.cpp transformation_function_call.cpp transformation_inline_function.cpp diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp index 3656e924a..ee638b6fe 100644 --- a/source/fuzz/fuzzer.cpp +++ b/source/fuzz/fuzzer.cpp @@ -71,6 +71,7 @@ #include "source/fuzz/fuzzer_pass_replace_linear_algebra_instructions.h" #include "source/fuzz/fuzzer_pass_replace_loads_stores_with_copy_memories.h" #include "source/fuzz/fuzzer_pass_replace_opphi_ids_from_dead_predecessors.h" +#include "source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h" #include "source/fuzz/fuzzer_pass_replace_parameter_with_global.h" #include "source/fuzz/fuzzer_pass_replace_params_with_struct.h" #include "source/fuzz/fuzzer_pass_split_blocks.h" @@ -325,6 +326,9 @@ Fuzzer::FuzzerResultStatus Fuzzer::Run( MaybeAddPass( &passes, ir_context.get(), &transformation_context, &fuzzer_context, transformation_sequence_out); + MaybeAddPass( + &passes, ir_context.get(), &transformation_context, &fuzzer_context, + transformation_sequence_out); MaybeAddPass( &passes, ir_context.get(), &transformation_context, &fuzzer_context, transformation_sequence_out); diff --git a/source/fuzz/fuzzer_context.cpp b/source/fuzz/fuzzer_context.cpp index 153b9ef6e..3443a1489 100644 --- a/source/fuzz/fuzzer_context.cpp +++ b/source/fuzz/fuzzer_context.cpp @@ -27,6 +27,8 @@ const std::pair kChanceOfAddingAccessChain = {5, 50}; const std::pair kChanceOfAddingAnotherStructField = {20, 90}; const std::pair kChanceOfAddingArrayOrStructType = {20, 90}; +const std::pair + kChanceOfAddingBothBranchesWhenReplacingOpSelect = {40, 60}; const std::pair kChanceOfAddingCompositeInsert = {20, 50}; const std::pair kChanceOfAddingCopyMemory = {20, 50}; const std::pair kChanceOfAddingDeadBlock = {20, 90}; @@ -48,6 +50,8 @@ const std::pair kChanceOfAddingParameters = {5, 70}; const std::pair kChanceOfAddingRelaxedDecoration = {20, 90}; const std::pair kChanceOfAddingStore = {5, 50}; const std::pair kChanceOfAddingSynonyms = {20, 50}; +const std::pair + kChanceOfAddingTrueBranchWhenReplacingOpSelect = {40, 60}; const std::pair kChanceOfAddingVectorType = {20, 70}; const std::pair kChanceOfAddingVectorShuffle = {20, 70}; const std::pair kChanceOfAdjustingBranchWeights = {20, 90}; @@ -105,6 +109,8 @@ const std::pair kChanceOfReplacingLoadStoreWithCopyMemory = {20, 90}; const std::pair kChanceOfReplacingOpPhiIdFromDeadPredecessor = {20, 90}; +const std::pair + kChanceOfReplacingOpSelectWithConditionalBranch = {20, 90}; const std::pair kChanceOfReplacingParametersWithGlobals = { 30, 70}; const std::pair kChanceOfReplacingParametersWithStruct = { @@ -162,6 +168,8 @@ FuzzerContext::FuzzerContext(RandomGenerator* random_generator, ChooseBetweenMinAndMax(kChanceOfAddingAnotherStructField); chance_of_adding_array_or_struct_type_ = ChooseBetweenMinAndMax(kChanceOfAddingArrayOrStructType); + chance_of_adding_both_branches_when_replacing_opselect_ = + ChooseBetweenMinAndMax(kChanceOfAddingBothBranchesWhenReplacingOpSelect); chance_of_adding_composite_insert_ = ChooseBetweenMinAndMax(kChanceOfAddingCompositeInsert); chance_of_adding_copy_memory_ = @@ -194,6 +202,8 @@ FuzzerContext::FuzzerContext(RandomGenerator* random_generator, chance_of_adding_relaxed_decoration_ = ChooseBetweenMinAndMax(kChanceOfAddingRelaxedDecoration); chance_of_adding_store_ = ChooseBetweenMinAndMax(kChanceOfAddingStore); + chance_of_adding_true_branch_when_replacing_opselect_ = + ChooseBetweenMinAndMax(kChanceOfAddingTrueBranchWhenReplacingOpSelect); chance_of_adding_vector_shuffle_ = ChooseBetweenMinAndMax(kChanceOfAddingVectorShuffle); chance_of_adding_vector_type_ = @@ -271,6 +281,8 @@ FuzzerContext::FuzzerContext(RandomGenerator* random_generator, ChooseBetweenMinAndMax(kChanceOfReplacingLoadStoreWithCopyMemory); chance_of_replacing_opphi_id_from_dead_predecessor_ = ChooseBetweenMinAndMax(kChanceOfReplacingOpPhiIdFromDeadPredecessor); + chance_of_replacing_opselect_with_conditional_branch_ = + ChooseBetweenMinAndMax(kChanceOfReplacingOpSelectWithConditionalBranch); chance_of_replacing_parameters_with_globals_ = ChooseBetweenMinAndMax(kChanceOfReplacingParametersWithGlobals); chance_of_replacing_parameters_with_struct_ = diff --git a/source/fuzz/fuzzer_context.h b/source/fuzz/fuzzer_context.h index 594cb9070..7427a4189 100644 --- a/source/fuzz/fuzzer_context.h +++ b/source/fuzz/fuzzer_context.h @@ -115,6 +115,9 @@ class FuzzerContext { uint32_t GetChanceOfAddingArrayOrStructType() { return chance_of_adding_array_or_struct_type_; } + uint32_t GetChanceOfAddingBothBranchesWhenReplacingOpSelect() { + return chance_of_adding_both_branches_when_replacing_opselect_; + } uint32_t GetChanceOfAddingCompositeInsert() { return chance_of_adding_composite_insert_; } @@ -157,6 +160,9 @@ class FuzzerContext { } uint32_t GetChanceOfAddingStore() { return chance_of_adding_store_; } uint32_t GetChanceOfAddingSynonyms() { return chance_of_adding_synonyms_; } + uint32_t GetChanceOfAddingTrueBranchWhenReplacingOpSelect() { + return chance_of_adding_true_branch_when_replacing_opselect_; + } uint32_t GetChanceOfAddingVectorShuffle() { return chance_of_adding_vector_shuffle_; } @@ -264,6 +270,9 @@ class FuzzerContext { uint32_t GetChanceOfReplacingOpPhiIdFromDeadPredecessor() { return chance_of_replacing_opphi_id_from_dead_predecessor_; } + uint32_t GetChanceOfReplacingOpselectWithConditionalBranch() { + return chance_of_replacing_opselect_with_conditional_branch_; + } uint32_t GetChanceOfReplacingParametersWithGlobals() { return chance_of_replacing_parameters_with_globals_; } @@ -364,6 +373,7 @@ class FuzzerContext { uint32_t chance_of_adding_access_chain_; uint32_t chance_of_adding_another_struct_field_; uint32_t chance_of_adding_array_or_struct_type_; + uint32_t chance_of_adding_both_branches_when_replacing_opselect_; uint32_t chance_of_adding_composite_insert_; uint32_t chance_of_adding_copy_memory_; uint32_t chance_of_adding_dead_block_; @@ -382,6 +392,7 @@ class FuzzerContext { uint32_t chance_of_adding_relaxed_decoration_; uint32_t chance_of_adding_store_; uint32_t chance_of_adding_synonyms_; + uint32_t chance_of_adding_true_branch_when_replacing_opselect_; uint32_t chance_of_adding_vector_shuffle_; uint32_t chance_of_adding_vector_type_; uint32_t chance_of_adjusting_branch_weights_; @@ -421,6 +432,7 @@ class FuzzerContext { uint32_t chance_of_replacing_linear_algebra_instructions_; uint32_t chance_of_replacing_load_store_with_copy_memory_; uint32_t chance_of_replacing_opphi_id_from_dead_predecessor_; + uint32_t chance_of_replacing_opselect_with_conditional_branch_; uint32_t chance_of_replacing_parameters_with_globals_; uint32_t chance_of_replacing_parameters_with_struct_; uint32_t chance_of_splitting_block_; diff --git a/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.cpp b/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.cpp new file mode 100644 index 000000000..0496268c5 --- /dev/null +++ b/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.cpp @@ -0,0 +1,162 @@ +// Copyright (c) 2020 Google LLC +// +// 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 "source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h" + +#include "source/fuzz/fuzzer_util.h" +#include "source/fuzz/instruction_descriptor.h" +#include "source/fuzz/transformation_replace_opselect_with_conditional_branch.h" +#include "source/fuzz/transformation_split_block.h" + +namespace spvtools { +namespace fuzz { + +FuzzerPassReplaceOpSelectsWithConditionalBranches:: + FuzzerPassReplaceOpSelectsWithConditionalBranches( + opt::IRContext* ir_context, + TransformationContext* transformation_context, + FuzzerContext* fuzzer_context, + protobufs::TransformationSequence* transformations) + : FuzzerPass(ir_context, transformation_context, fuzzer_context, + transformations) {} + +FuzzerPassReplaceOpSelectsWithConditionalBranches:: + ~FuzzerPassReplaceOpSelectsWithConditionalBranches() = default; + +void FuzzerPassReplaceOpSelectsWithConditionalBranches::Apply() { + // Keep track of the instructions that we want to replace. We need to collect + // them in a vector, since it's not safe to modify the module while iterating + // over it. + std::vector replaceable_opselect_instruction_ids; + + // Loop over all the instructions in the module. + for (auto& function : *GetIRContext()->module()) { + for (auto& block : function) { + // We cannot split loop headers, so we don't need to consider instructions + // in loop headers that are also merge blocks (since they would need to be + // split). + if (block.IsLoopHeader() && + GetIRContext()->GetStructuredCFGAnalysis()->IsMergeBlock( + block.id())) { + continue; + } + + for (auto& instruction : block) { + // We only care about OpSelect instructions. + if (instruction.opcode() != SpvOpSelect) { + continue; + } + + // Randomly choose whether to consider this instruction for replacement. + if (!GetFuzzerContext()->ChoosePercentage( + GetFuzzerContext() + ->GetChanceOfReplacingOpselectWithConditionalBranch())) { + continue; + } + + // If the block is a loop header and we need to split it, the + // transformation cannot be applied because loop headers cannot be + // split. We can break out of this loop because the transformation can + // only be applied to at most the first instruction in a loop header. + if (block.IsLoopHeader() && InstructionNeedsSplitBefore(&instruction)) { + break; + } + + // If the instruction separates an OpSampledImage from its use, the + // block cannot be split around it and the instruction cannot be + // replaced. + if (fuzzerutil:: + SplittingBeforeInstructionSeparatesOpSampledImageDefinitionFromUse( + &block, &instruction)) { + continue; + } + + // We can apply the transformation to this instruction. + replaceable_opselect_instruction_ids.push_back(instruction.result_id()); + } + } + } + + // Apply the transformations, splitting the blocks containing the + // instructions, if necessary. + for (uint32_t instruction_id : replaceable_opselect_instruction_ids) { + auto instruction = + GetIRContext()->get_def_use_mgr()->GetDef(instruction_id); + + // If the instruction requires the block containing it to be split before + // it, split the block. + if (InstructionNeedsSplitBefore(instruction)) { + ApplyTransformation(TransformationSplitBlock( + MakeInstructionDescriptor(GetIRContext(), instruction), + GetFuzzerContext()->GetFreshId())); + } + + // Decide whether to have two branches or just one. + bool two_branches = GetFuzzerContext()->ChoosePercentage( + GetFuzzerContext() + ->GetChanceOfAddingBothBranchesWhenReplacingOpSelect()); + + // If there will be only one branch, decide whether it will be the true + // branch or the false branch. + bool true_branch_id_zero = + !two_branches && + GetFuzzerContext()->ChoosePercentage( + GetFuzzerContext() + ->GetChanceOfAddingTrueBranchWhenReplacingOpSelect()); + bool false_branch_id_zero = !two_branches && !true_branch_id_zero; + + uint32_t true_branch_id = + true_branch_id_zero ? 0 : GetFuzzerContext()->GetFreshId(); + uint32_t false_branch_id = + false_branch_id_zero ? 0 : GetFuzzerContext()->GetFreshId(); + + ApplyTransformation(TransformationReplaceOpSelectWithConditionalBranch( + instruction_id, true_branch_id, false_branch_id)); + } +} + +bool FuzzerPassReplaceOpSelectsWithConditionalBranches:: + InstructionNeedsSplitBefore(opt::Instruction* instruction) { + assert(instruction && instruction->opcode() == SpvOpSelect && + "The instruction must be OpSelect."); + + auto block = GetIRContext()->get_instr_block(instruction); + assert(block && "The instruction must be contained in a block."); + + // We need to split the block if the instruction is not the first in its + // block. + if (instruction->unique_id() != block->begin()->unique_id()) { + return true; + } + + // We need to split the block if it is a merge block. + if (GetIRContext()->GetStructuredCFGAnalysis()->IsMergeBlock(block->id())) { + return true; + } + + // We need to split the block if it has more than one predecessor. + if (GetIRContext()->cfg()->preds(block->id()).size() != 1) { + return true; + } + + // We need to split the block if its predecessor is a header or it does not + // branch unconditionally to the block. + auto predecessor = GetIRContext()->get_instr_block( + GetIRContext()->cfg()->preds(block->id())[0]); + return predecessor->MergeBlockIdIfAny() || + predecessor->terminator()->opcode() != SpvOpBranch; +} + +} // namespace fuzz +} // namespace spvtools diff --git a/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h b/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h new file mode 100644 index 000000000..ef3ec5784 --- /dev/null +++ b/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h @@ -0,0 +1,53 @@ +// Copyright (c) 2020 Google LLC +// +// 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. + +#ifndef SOURCE_FUZZ_FUZZER_PASS_REPLACE_OPSELECTS_WITH_CONDITIONAL_BRANCHES_ +#define SOURCE_FUZZ_FUZZER_PASS_REPLACE_OPSELECTS_WITH_CONDITIONAL_BRANCHES_ + +#include "source/fuzz/fuzzer_pass.h" + +namespace spvtools { +namespace fuzz { + +// A fuzzer pass to replace OpSelect instructions (where the condition is a +// scalar boolean) with conditional branches and OpPhi instructions. +class FuzzerPassReplaceOpSelectsWithConditionalBranches : public FuzzerPass { + public: + FuzzerPassReplaceOpSelectsWithConditionalBranches( + opt::IRContext* ir_context, TransformationContext* transformation_context, + FuzzerContext* fuzzer_context, + protobufs::TransformationSequence* transformations); + + ~FuzzerPassReplaceOpSelectsWithConditionalBranches() override; + + void Apply() override; + + private: + // Returns true if any of the following holds: + // - the instruction is not the first in its block + // - the block containing it is a merge block + // - the block does not have a unique predecessor + // - the predecessor of the block is the header of a construct + // - the predecessor does not branch unconditionally to the block + // If this function returns true, the block must be split before the + // instruction for TransformationReplaceOpSelectWithConditionalBranch to be + // applicable. + // Assumes that the instruction is OpSelect. + bool InstructionNeedsSplitBefore(opt::Instruction* instruction); +}; + +} // namespace fuzz +} // namespace spvtools + +#endif // SOURCE_FUZZ_FUZZER_PASS_REPLACE_OPSELECTS_WITH_CONDITIONAL_BRANCHES_ diff --git a/source/fuzz/fuzzer_util.cpp b/source/fuzz/fuzzer_util.cpp index aa45d6692..c6d18c093 100644 --- a/source/fuzz/fuzzer_util.cpp +++ b/source/fuzz/fuzzer_util.cpp @@ -1486,6 +1486,39 @@ bool MembersHaveBuiltInDecoration(opt::IRContext* ir_context, return builtin_count != 0; } +bool SplittingBeforeInstructionSeparatesOpSampledImageDefinitionFromUse( + opt::BasicBlock* block_to_split, opt::Instruction* split_before) { + std::set sampled_image_result_ids; + bool before_split = true; + + // Check all the instructions in the block to split. + for (auto& instruction : *block_to_split) { + if (&instruction == &*split_before) { + before_split = false; + } + if (before_split) { + // If the instruction comes before the split and its opcode is + // OpSampledImage, record its result id. + if (instruction.opcode() == SpvOpSampledImage) { + sampled_image_result_ids.insert(instruction.result_id()); + } + } else { + // If the instruction comes after the split, check if ids + // corresponding to OpSampledImage instructions defined before the split + // are used, and return true if they are. + if (!instruction.WhileEachInId( + [&sampled_image_result_ids](uint32_t* id) -> bool { + return !sampled_image_result_ids.count(*id); + })) { + return true; + } + } + } + + // No usage that would be separated from the definition has been found. + return false; +} + } // namespace fuzzerutil } // namespace fuzz } // namespace spvtools diff --git a/source/fuzz/fuzzer_util.h b/source/fuzz/fuzzer_util.h index 865c1a081..2496c1169 100644 --- a/source/fuzz/fuzzer_util.h +++ b/source/fuzz/fuzzer_util.h @@ -524,6 +524,11 @@ bool IdUseCanBeReplaced(opt::IRContext* ir_context, bool MembersHaveBuiltInDecoration(opt::IRContext* ir_context, uint32_t struct_type_id); +// Returns true iff splitting block |block_to_split| just before the instruction +// |split_before| would separate an OpSampledImage instruction from its usage. +bool SplittingBeforeInstructionSeparatesOpSampledImageDefinitionFromUse( + opt::BasicBlock* block_to_split, opt::Instruction* split_before); + } // namespace fuzzerutil } // namespace fuzz } // namespace spvtools diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto index 0cc7a78eb..5d27761f8 100644 --- a/source/fuzz/protobufs/spvtoolsfuzz.proto +++ b/source/fuzz/protobufs/spvtoolsfuzz.proto @@ -418,6 +418,7 @@ message Transformation { TransformationMutatePointer mutate_pointer = 71; TransformationReplaceIrrelevantId replace_irrelevant_id = 72; TransformationReplaceOpPhiIdFromDeadPredecessor replace_opphi_id_from_dead_predecessor = 73; + TransformationReplaceOpSelectWithConditionalBranch replace_opselect_with_conditional_branch = 74; // Add additional option using the next available number. } } @@ -1564,6 +1565,39 @@ message TransformationReplaceOpPhiIdFromDeadPredecessor { } +message TransformationReplaceOpSelectWithConditionalBranch { + + // A transformation that takes an OpSelect instruction with a + // scalar boolean condition and replaces it with a conditional + // branch and an OpPhi instruction. + // The OpSelect instruction must be the first instruction in its + // block, which must have a unique predecessor. The block will + // become the merge block of a new construct, while its predecessor + // will become the header. + // Given the original OpSelect instruction: + // %id = OpSelect %type %cond %then %else + // The branching instruction of the header will be: + // OpBranchConditional %cond %true_block_id %false_block_id + // and the OpSelect instruction will be turned into: + // %id = OpPhi %type %then %true_block_id %else %false_block_id + // At most one of |true_block_id| and |false_block_id| can be zero. In + // that case, there will be no such block and all references to it + // will be replaced by %merge_block (where %merge_block is the + // block containing the OpSelect instruction). + + // The result id of the OpSelect instruction. + uint32 select_id = 1; + + // A fresh id for the new block that the predecessor of the block + // containing |select_id| will branch to if the condition holds. + uint32 true_block_id = 2; + + // A fresh id for the new block that the predecessor of the block + // containing |select_id| will branch to if the condition does not + // hold. + uint32 false_block_id = 3; +} + message TransformationReplaceParamsWithStruct { // Replaces parameters of the function with a struct containing diff --git a/source/fuzz/transformation.cpp b/source/fuzz/transformation.cpp index 236090133..7156f6e08 100644 --- a/source/fuzz/transformation.cpp +++ b/source/fuzz/transformation.cpp @@ -78,6 +78,7 @@ #include "source/fuzz/transformation_replace_linear_algebra_instruction.h" #include "source/fuzz/transformation_replace_load_store_with_copy_memory.h" #include "source/fuzz/transformation_replace_opphi_id_from_dead_predecessor.h" +#include "source/fuzz/transformation_replace_opselect_with_conditional_branch.h" #include "source/fuzz/transformation_replace_parameter_with_global.h" #include "source/fuzz/transformation_replace_params_with_struct.h" #include "source/fuzz/transformation_set_function_control.h" @@ -276,6 +277,10 @@ std::unique_ptr Transformation::FromMessage( kReplaceLoadStoreWithCopyMemory: return MakeUnique( message.replace_load_store_with_copy_memory()); + case protobufs::Transformation::TransformationCase:: + kReplaceOpselectWithConditionalBranch: + return MakeUnique( + message.replace_opselect_with_conditional_branch()); case protobufs::Transformation::TransformationCase:: kReplaceParameterWithGlobal: return MakeUnique( diff --git a/source/fuzz/transformation_replace_opselect_with_conditional_branch.cpp b/source/fuzz/transformation_replace_opselect_with_conditional_branch.cpp new file mode 100644 index 000000000..5ae56fd22 --- /dev/null +++ b/source/fuzz/transformation_replace_opselect_with_conditional_branch.cpp @@ -0,0 +1,204 @@ +// Copyright (c) 2020 Google LLC +// +// 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 "source/fuzz/transformation_replace_opselect_with_conditional_branch.h" + +#include "source/fuzz/fuzzer_util.h" + +namespace spvtools { +namespace fuzz { +TransformationReplaceOpSelectWithConditionalBranch:: + TransformationReplaceOpSelectWithConditionalBranch( + const spvtools::fuzz::protobufs:: + TransformationReplaceOpSelectWithConditionalBranch& message) + : message_(message) {} + +TransformationReplaceOpSelectWithConditionalBranch:: + TransformationReplaceOpSelectWithConditionalBranch( + uint32_t select_id, uint32_t true_block_id, uint32_t false_block_id) { + message_.set_select_id(select_id); + message_.set_true_block_id(true_block_id); + message_.set_false_block_id(false_block_id); +} + +bool TransformationReplaceOpSelectWithConditionalBranch::IsApplicable( + opt::IRContext* ir_context, + const TransformationContext& /* unused */) const { + assert((message_.true_block_id() || message_.false_block_id()) && + "At least one of the ids must be non-zero."); + + // Check that the non-zero ids are fresh. + std::set used_ids; + for (uint32_t id : {message_.true_block_id(), message_.false_block_id()}) { + if (id && !CheckIdIsFreshAndNotUsedByThisTransformation(id, ir_context, + &used_ids)) { + return false; + } + } + + auto instruction = + ir_context->get_def_use_mgr()->GetDef(message_.select_id()); + + // The instruction must exist and it must be an OpSelect instruction. + if (!instruction || instruction->opcode() != SpvOpSelect) { + return false; + } + + // Check that the condition is a scalar boolean. + auto condition = ir_context->get_def_use_mgr()->GetDef( + instruction->GetSingleWordInOperand(0)); + assert(condition && "The condition should always exist in a valid module."); + + auto condition_type = + ir_context->get_type_mgr()->GetType(condition->type_id()); + if (!condition_type->AsBool()) { + return false; + } + + auto block = ir_context->get_instr_block(instruction); + assert(block && "The block containing the instruction must be found"); + + // The instruction must be the first in its block. + if (instruction->unique_id() != block->begin()->unique_id()) { + return false; + } + + // The block must not be a merge block. + if (ir_context->GetStructuredCFGAnalysis()->IsMergeBlock(block->id())) { + return false; + } + + // The block must have exactly one predecessor. + auto predecessors = ir_context->cfg()->preds(block->id()); + if (predecessors.size() != 1) { + return false; + } + + uint32_t pred_id = predecessors[0]; + auto predecessor = ir_context->get_instr_block(pred_id); + + // The predecessor must not be the header of a construct and it must end with + // OpBranch. + if (predecessor->GetMergeInst() != nullptr || + predecessor->terminator()->opcode() != SpvOpBranch) { + return false; + } + + return true; +} + +void TransformationReplaceOpSelectWithConditionalBranch::Apply( + opt::IRContext* ir_context, TransformationContext* /* unused */) const { + auto instruction = + ir_context->get_def_use_mgr()->GetDef(message_.select_id()); + + auto block = ir_context->get_instr_block(instruction); + + auto predecessor = + ir_context->get_instr_block(ir_context->cfg()->preds(block->id())[0]); + + // Create a new block for each non-zero id in {|message_.true_branch_id|, + // |message_.false_branch_id|}. Make each newly-created block branch + // unconditionally to the instruction block. + for (uint32_t id : {message_.true_block_id(), message_.false_block_id()}) { + if (id) { + fuzzerutil::UpdateModuleIdBound(ir_context, id); + + // Create the new block. + auto new_block = MakeUnique(MakeUnique( + ir_context, SpvOpLabel, 0, id, opt::Instruction::OperandList{})); + + // Add an unconditional branch from the new block to the instruction + // block. + new_block->AddInstruction(MakeUnique( + ir_context, SpvOpBranch, 0, 0, + opt::Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {block->id()}}})); + + // Insert the new block right after the predecessor of the instruction + // block. + block->GetParent()->InsertBasicBlockBefore(std::move(new_block), block); + } + } + + // Delete the OpBranch instruction from the predecessor. + ir_context->KillInst(predecessor->terminator()); + + // Add an OpSelectionMerge instruction to the predecessor block, where the + // merge block is the instruction block. + predecessor->AddInstruction(MakeUnique( + ir_context, SpvOpSelectionMerge, 0, 0, + opt::Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {block->id()}}, + {SPV_OPERAND_TYPE_SELECTION_CONTROL, + {SpvSelectionControlMaskNone}}})); + + // |if_block| will be the true block, if it has been created, the instruction + // block otherwise. + uint32_t if_block = + message_.true_block_id() ? message_.true_block_id() : block->id(); + + // |else_block| will be the false block, if it has been created, the + // instruction block otherwise. + uint32_t else_block = + message_.false_block_id() ? message_.false_block_id() : block->id(); + + assert(if_block != else_block && + "|if_block| and |else_block| should always be different, if the " + "transformation is applicable."); + + // Add a conditional branching instruction to the predecessor, branching to + // |if_block| if the condition is true and to |if_false| otherwise. + predecessor->AddInstruction(MakeUnique( + ir_context, SpvOpBranchConditional, 0, 0, + opt::Instruction::OperandList{ + {SPV_OPERAND_TYPE_ID, {instruction->GetSingleWordInOperand(0)}}, + {SPV_OPERAND_TYPE_ID, {if_block}}, + {SPV_OPERAND_TYPE_ID, {else_block}}})); + + // |if_pred| will be the true block, if it has been created, the existing + // predecessor otherwise. + uint32_t if_pred = + message_.true_block_id() ? message_.true_block_id() : predecessor->id(); + + // |else_pred| will be the false block, if it has been created, the existing + // predecessor otherwise. + uint32_t else_pred = + message_.false_block_id() ? message_.false_block_id() : predecessor->id(); + + // Replace the OpSelect instruction in the merge block with an OpPhi. + // This: OpSelect %type %cond %if %else + // will become: OpPhi %type %if %if_pred %else %else_pred + instruction->SetOpcode(SpvOpPhi); + std::vector operands; + + operands.emplace_back(instruction->GetInOperand(1)); + operands.emplace_back(opt::Operand{SPV_OPERAND_TYPE_ID, {if_pred}}); + + operands.emplace_back(instruction->GetInOperand(2)); + operands.emplace_back(opt::Operand{SPV_OPERAND_TYPE_ID, {else_pred}}); + + instruction->SetInOperands(std::move(operands)); + + // Invalidate all analyses, since the structure of the module was changed. + ir_context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone); +} + +protobufs::Transformation +TransformationReplaceOpSelectWithConditionalBranch::ToMessage() const { + protobufs::Transformation result; + *result.mutable_replace_opselect_with_conditional_branch() = message_; + return result; +} + +} // namespace fuzz +} // namespace spvtools diff --git a/source/fuzz/transformation_replace_opselect_with_conditional_branch.h b/source/fuzz/transformation_replace_opselect_with_conditional_branch.h new file mode 100644 index 000000000..612c6468d --- /dev/null +++ b/source/fuzz/transformation_replace_opselect_with_conditional_branch.h @@ -0,0 +1,61 @@ +// Copyright (c) 2020 Google LLC +// +// 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. + +#ifndef SOURCE_FUZZ_TRANSFORMATION_REPLACE_OPSELECT_WITH_CONDITIONAL_BRANCH_H +#define SOURCE_FUZZ_TRANSFORMATION_REPLACE_OPSELECT_WITH_CONDITIONAL_BRANCH_H + +#include "source/fuzz/transformation.h" + +namespace spvtools { +namespace fuzz { + +class TransformationReplaceOpSelectWithConditionalBranch + : public Transformation { + public: + explicit TransformationReplaceOpSelectWithConditionalBranch( + const protobufs::TransformationReplaceOpSelectWithConditionalBranch& + message); + + TransformationReplaceOpSelectWithConditionalBranch(uint32_t select_id, + uint32_t true_block_id, + uint32_t false_block_id); + + // - |message_.select_id| is the result id of an OpSelect instruction. + // - The condition of the OpSelect must be a scalar boolean. + // - The OpSelect instruction is the first instruction in its block. + // - The block containing the instruction is not a merge block, and it has a + // single predecessor, which is not a header and whose last instruction is + // OpBranch. + // - Each of |message_.true_block_id| and |message_.false_block_id| is either + // 0 or a valid fresh id, and at most one of them is 0. They must be + // distinct. + bool IsApplicable( + opt::IRContext* ir_context, + const TransformationContext& transformation_context) const override; + + // Replaces the OpSelect instruction with id |message_.select_id| with a + // conditional branch and an OpPhi instruction. + void Apply(opt::IRContext* ir_context, + TransformationContext* transformation_context) const override; + + protobufs::Transformation ToMessage() const override; + + private: + protobufs::TransformationReplaceOpSelectWithConditionalBranch message_; +}; + +} // namespace fuzz +} // namespace spvtools + +#endif // SOURCE_FUZZ_TRANSFORMATION_REPLACE_OPSELECT_WITH_CONDITIONAL_BRANCH_H diff --git a/source/fuzz/transformation_split_block.cpp b/source/fuzz/transformation_split_block.cpp index 3c437e432..5e2babacf 100644 --- a/source/fuzz/transformation_split_block.cpp +++ b/source/fuzz/transformation_split_block.cpp @@ -83,27 +83,9 @@ bool TransformationSplitBlock::IsApplicable( // Splitting the block must not separate the definition of an OpSampledImage // from its use: the SPIR-V data rules require them to be in the same block. - std::set sampled_image_result_ids; - bool before_split = true; - for (auto& instruction : *block_to_split) { - if (&instruction == &*split_before) { - before_split = false; - } - if (before_split) { - if (instruction.opcode() == SpvOpSampledImage) { - sampled_image_result_ids.insert(instruction.result_id()); - } - } else { - if (!instruction.WhileEachInId( - [&sampled_image_result_ids](uint32_t* id) -> bool { - return !sampled_image_result_ids.count(*id); - })) { - return false; - } - } - } - - return true; + return !fuzzerutil:: + SplittingBeforeInstructionSeparatesOpSampledImageDefinitionFromUse( + block_to_split, instruction_to_split_before); } void TransformationSplitBlock::Apply( diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt index 34317d004..6199e6902 100644 --- a/test/fuzz/CMakeLists.txt +++ b/test/fuzz/CMakeLists.txt @@ -87,6 +87,7 @@ if (${SPIRV_BUILD_FUZZER}) transformation_replace_linear_algebra_instruction_test.cpp transformation_replace_load_store_with_copy_memory_test.cpp transformation_replace_opphi_id_from_dead_predecessor_test.cpp + transformation_replace_opselect_with_conditional_branch_test.cpp transformation_replace_parameter_with_global_test.cpp transformation_replace_params_with_struct_test.cpp transformation_set_function_control_test.cpp diff --git a/test/fuzz/transformation_replace_opselect_with_conditional_branch_test.cpp b/test/fuzz/transformation_replace_opselect_with_conditional_branch_test.cpp new file mode 100644 index 000000000..b6a5138a0 --- /dev/null +++ b/test/fuzz/transformation_replace_opselect_with_conditional_branch_test.cpp @@ -0,0 +1,278 @@ +// Copyright (c) 2020 Google LLC +// +// 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 "source/fuzz/transformation_replace_opselect_with_conditional_branch.h" + +#include "test/fuzz/fuzz_test_util.h" + +#include "source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h" +#include "source/fuzz/pseudo_random_generator.h" + +namespace spvtools { +namespace fuzz { +namespace { + +TEST(TransformationReplaceOpSelectWithConditionalBranchTest, Inapplicable) { + std::string shader = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %2 "main" + OpExecutionMode %2 OriginUpperLeft + OpSource ESSL 310 + %3 = OpTypeVoid + %4 = OpTypeFunction %3 + %5 = OpTypeInt 32 1 + %6 = OpConstant %5 1 + %7 = OpConstant %5 2 + %8 = OpTypeVector %5 4 + %9 = OpConstantNull %8 + %10 = OpConstantComposite %8 %6 %6 %7 %7 + %11 = OpTypeBool + %12 = OpTypeVector %11 4 + %13 = OpConstantTrue %11 + %14 = OpConstantFalse %11 + %15 = OpConstantComposite %12 %13 %14 %14 %13 + %2 = OpFunction %3 None %4 + %16 = OpLabel + %17 = OpCopyObject %5 %6 + %18 = OpCopyObject %5 %7 + OpBranch %19 + %19 = OpLabel + %20 = OpCopyObject %5 %17 + %21 = OpSelect %5 %13 %17 %18 + OpBranch %22 + %22 = OpLabel + %23 = OpSelect %8 %15 %9 %10 + OpBranch %24 + %24 = OpLabel + OpSelectionMerge %25 None + OpBranchConditional %13 %26 %27 + %26 = OpLabel + %28 = OpSelect %5 %13 %17 %18 + OpBranch %27 + %27 = OpLabel + %29 = OpSelect %5 %13 %17 %18 + OpBranch %25 + %25 = OpLabel + %30 = OpSelect %5 %13 %17 %18 + OpBranch %31 + %31 = OpLabel + OpLoopMerge %32 %33 None + OpBranch %33 + %33 = OpLabel + %34 = OpSelect %5 %13 %17 %18 + OpBranchConditional %13 %31 %32 + %32 = OpLabel + %35 = OpSelect %5 %13 %17 %18 + OpBranch %36 + %36 = OpLabel + %37 = OpSelect %5 %13 %17 %18 + OpReturn + OpFunctionEnd +)"; + const auto env = SPV_ENV_UNIVERSAL_1_5; + const auto consumer = nullptr; + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + ASSERT_TRUE(IsValid(env, context.get())); + + FactManager fact_manager; + spvtools::ValidatorOptions validator_options; + TransformationContext transformation_context(&fact_manager, + validator_options); + + // %20 is not an OpSelect instruction. + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(20, 100, 101) + .IsApplicable(context.get(), transformation_context)); + + // %21 is not the first instruction in its block. + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(21, 100, 101) + .IsApplicable(context.get(), transformation_context)); + + // The condition for %23 is not a scalar, but a vector of booleans. + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(23, 100, 101) + .IsApplicable(context.get(), transformation_context)); + + // The predecessor (%24) of the block containing %28 is the header of a + // selection construct and does not branch unconditionally. + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(24, 100, 101) + .IsApplicable(context.get(), transformation_context)); + + // The block containing %29 has two predecessors (%24 and %26). + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(29, 100, 101) + .IsApplicable(context.get(), transformation_context)); + + // The block containing %30 is the merge block for a selection construct. + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(30, 100, 101) + .IsApplicable(context.get(), transformation_context)); + + // The predecessor (%31) of the block containing %34 is a loop header. + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(31, 100, 101) + .IsApplicable(context.get(), transformation_context)); + + // The block containing %35 is the merge block for a loop construct. + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(35, 100, 101) + .IsApplicable(context.get(), transformation_context)); + +#ifndef NDEBUG + // |true_block_id| and |false_block_id| are both 0. + ASSERT_DEATH( + TransformationReplaceOpSelectWithConditionalBranch(37, 0, 0).IsApplicable( + context.get(), transformation_context), + "At least one of the ids must be non-zero."); +#endif + + // The fresh ids are not distinct. + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(37, 100, 100) + .IsApplicable(context.get(), transformation_context)); + + // One of the ids is not fresh. + ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(37, 100, 10) + .IsApplicable(context.get(), transformation_context)); +} + +TEST(TransformationReplaceOpSelectWithConditionalBranchTest, Simple) { + std::string shader = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %2 "main" + OpExecutionMode %2 OriginUpperLeft + OpSource ESSL 310 + OpName %2 "main" + %3 = OpTypeVoid + %4 = OpTypeFunction %3 + %5 = OpTypeInt 32 1 + %6 = OpConstant %5 1 + %7 = OpConstant %5 2 + %8 = OpTypeVector %5 4 + %9 = OpConstantNull %8 + %10 = OpConstantComposite %8 %6 %6 %7 %7 + %11 = OpTypeBool + %12 = OpTypeVector %11 4 + %13 = OpConstantTrue %11 + %14 = OpConstantFalse %11 + %15 = OpConstantComposite %12 %13 %14 %14 %13 + %2 = OpFunction %3 None %4 + %16 = OpLabel + %17 = OpCopyObject %5 %6 + %18 = OpCopyObject %5 %7 + OpBranch %19 + %19 = OpLabel + %20 = OpSelect %5 %13 %17 %18 + OpSelectionMerge %21 None + OpBranchConditional %13 %22 %21 + %22 = OpLabel + OpBranch %23 + %23 = OpLabel + %24 = OpSelect %8 %13 %9 %10 + OpBranch %21 + %21 = OpLabel + OpBranch %25 + %25 = OpLabel + %26 = OpSelect %5 %13 %17 %18 + OpReturn + OpFunctionEnd +)"; + + const auto env = SPV_ENV_UNIVERSAL_1_5; + const auto consumer = nullptr; + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + ASSERT_TRUE(IsValid(env, context.get())); + + FactManager fact_manager; + spvtools::ValidatorOptions validator_options; + TransformationContext transformation_context(&fact_manager, + validator_options); + + auto transformation = + TransformationReplaceOpSelectWithConditionalBranch(20, 100, 101); + ASSERT_TRUE( + transformation.IsApplicable(context.get(), transformation_context)); + transformation.Apply(context.get(), &transformation_context); + + auto transformation2 = + TransformationReplaceOpSelectWithConditionalBranch(24, 0, 102); + ASSERT_TRUE( + transformation2.IsApplicable(context.get(), transformation_context)); + transformation2.Apply(context.get(), &transformation_context); + + auto transformation3 = + TransformationReplaceOpSelectWithConditionalBranch(26, 103, 0); + ASSERT_TRUE( + transformation3.IsApplicable(context.get(), transformation_context)); + transformation3.Apply(context.get(), &transformation_context); + + ASSERT_TRUE(IsValid(env, context.get())); + + std::string after_transformation = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %2 "main" + OpExecutionMode %2 OriginUpperLeft + OpSource ESSL 310 + OpName %2 "main" + %3 = OpTypeVoid + %4 = OpTypeFunction %3 + %5 = OpTypeInt 32 1 + %6 = OpConstant %5 1 + %7 = OpConstant %5 2 + %8 = OpTypeVector %5 4 + %9 = OpConstantNull %8 + %10 = OpConstantComposite %8 %6 %6 %7 %7 + %11 = OpTypeBool + %12 = OpTypeVector %11 4 + %13 = OpConstantTrue %11 + %14 = OpConstantFalse %11 + %15 = OpConstantComposite %12 %13 %14 %14 %13 + %2 = OpFunction %3 None %4 + %16 = OpLabel + %17 = OpCopyObject %5 %6 + %18 = OpCopyObject %5 %7 + OpSelectionMerge %19 None + OpBranchConditional %13 %100 %101 + %100 = OpLabel + OpBranch %19 + %101 = OpLabel + OpBranch %19 + %19 = OpLabel + %20 = OpPhi %5 %17 %100 %18 %101 + OpSelectionMerge %21 None + OpBranchConditional %13 %22 %21 + %22 = OpLabel + OpSelectionMerge %23 None + OpBranchConditional %13 %23 %102 + %102 = OpLabel + OpBranch %23 + %23 = OpLabel + %24 = OpPhi %8 %9 %22 %10 %102 + OpBranch %21 + %21 = OpLabel + OpSelectionMerge %25 None + OpBranchConditional %13 %103 %25 + %103 = OpLabel + OpBranch %25 + %25 = OpLabel + %26 = OpPhi %5 %17 %103 %18 %21 + OpReturn + OpFunctionEnd +)"; + + ASSERT_TRUE(IsEqual(env, after_transformation, context.get())); +} + +} // namespace +} // namespace fuzz +} // namespace spvtools