mirror of
https://github.com/RPCSX/SPIRV-Tools.git
synced 2026-01-31 01:05:19 +01:00
This change implements instruction folding for arithmetic operations that are redundant, specifically: x + 0 = 0 + x = x x - 0 = x 0 - x = -x x * 0 = 0 * x = 0 x * 1 = 1 * x = x 0 / x = 0 x / 1 = x mix(a, b, 0) = a mix(a, b, 1) = b Cache ExtInst import id in feature manager This allows us to avoid string lookups during optimization; for now we just cache GLSL std450 import id but I can imagine caching more sets as they become utilized by the optimizer. Add tests for add/sub/mul/div/mix folding The tests cover scalar float/double cases, and some vector cases. Since most of the code for floating point folding is shared, the tests for vector folding are not as exhaustive as scalar. To test sub->negate folding I had to implement a custom fixture.
556 lines
19 KiB
C++
556 lines
19 KiB
C++
// Copyright (c) 2018 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 "folding_rules.h"
|
|
#include "latest_version_glsl_std_450_header.h"
|
|
|
|
namespace spvtools {
|
|
namespace opt {
|
|
|
|
namespace {
|
|
const uint32_t kExtractCompositeIdInIdx = 0;
|
|
const uint32_t kInsertObjectIdInIdx = 0;
|
|
const uint32_t kInsertCompositeIdInIdx = 1;
|
|
const uint32_t kExtInstSetIdInIdx = 0;
|
|
const uint32_t kExtInstInstructionInIdx = 1;
|
|
const uint32_t kFMixXIdInIdx = 2;
|
|
const uint32_t kFMixYIdInIdx = 3;
|
|
|
|
FoldingRule IntMultipleBy1() {
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>& constants) {
|
|
assert(inst->opcode() == SpvOpIMul && "Wrong opcode. Should be OpIMul.");
|
|
for (uint32_t i = 0; i < 2; i++) {
|
|
if (constants[i] == nullptr) {
|
|
continue;
|
|
}
|
|
const analysis::IntConstant* int_constant = constants[i]->AsIntConstant();
|
|
if (int_constant && int_constant->GetU32BitValue() == 1) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands(
|
|
{{SPV_OPERAND_TYPE_ID, {inst->GetSingleWordInOperand(1 - i)}}});
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
|
|
FoldingRule CompositeConstructFeedingExtract() {
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>&) {
|
|
// If the input to an OpCompositeExtract is an OpCompositeConstruct,
|
|
// then we can simply use the appropriate element in the construction.
|
|
assert(inst->opcode() == SpvOpCompositeExtract &&
|
|
"Wrong opcode. Should be OpCompositeExtract.");
|
|
analysis::DefUseManager* def_use_mgr = inst->context()->get_def_use_mgr();
|
|
analysis::TypeManager* type_mgr = inst->context()->get_type_mgr();
|
|
uint32_t cid = inst->GetSingleWordInOperand(kExtractCompositeIdInIdx);
|
|
ir::Instruction* cinst = def_use_mgr->GetDef(cid);
|
|
|
|
if (cinst->opcode() != SpvOpCompositeConstruct) {
|
|
return false;
|
|
}
|
|
|
|
std::vector<ir::Operand> operands;
|
|
analysis::Type* composite_type = type_mgr->GetType(cinst->type_id());
|
|
if (composite_type->AsVector() == nullptr) {
|
|
// Get the element being extracted from the OpCompositeConstruct
|
|
// Since it is not a vector, it is simple to extract the single element.
|
|
uint32_t element_index = inst->GetSingleWordInOperand(1);
|
|
uint32_t element_id = cinst->GetSingleWordInOperand(element_index);
|
|
operands.push_back({SPV_OPERAND_TYPE_ID, {element_id}});
|
|
|
|
// Add the remaining indices for extraction.
|
|
for (uint32_t i = 2; i < inst->NumInOperands(); ++i) {
|
|
operands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
|
{inst->GetSingleWordInOperand(i)}});
|
|
}
|
|
|
|
} else {
|
|
// With vectors we have to handle the case where it is concatenating
|
|
// vectors.
|
|
assert(inst->NumInOperands() == 2 &&
|
|
"Expecting a vector of scalar values.");
|
|
|
|
uint32_t element_index = inst->GetSingleWordInOperand(1);
|
|
for (uint32_t construct_index = 0;
|
|
construct_index < cinst->NumInOperands(); ++construct_index) {
|
|
uint32_t element_id = cinst->GetSingleWordInOperand(construct_index);
|
|
ir::Instruction* element_def = def_use_mgr->GetDef(element_id);
|
|
analysis::Vector* element_type =
|
|
type_mgr->GetType(element_def->type_id())->AsVector();
|
|
if (element_type) {
|
|
uint32_t vector_size = element_type->element_count();
|
|
if (vector_size < element_index) {
|
|
// The element we want comes after this vector.
|
|
element_index -= vector_size;
|
|
} else {
|
|
// We want an element of this vector.
|
|
operands.push_back({SPV_OPERAND_TYPE_ID, {element_id}});
|
|
operands.push_back(
|
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {element_index}});
|
|
break;
|
|
}
|
|
} else {
|
|
if (element_index == 0) {
|
|
// This is a scalar, and we this is the element we are extracting.
|
|
operands.push_back({SPV_OPERAND_TYPE_ID, {element_id}});
|
|
break;
|
|
} else {
|
|
// Skip over this scalar value.
|
|
--element_index;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there were no extra indices, then we have the final object. No need
|
|
// to extract even more.
|
|
if (operands.size() == 1) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
}
|
|
|
|
inst->SetInOperands(std::move(operands));
|
|
return true;
|
|
};
|
|
}
|
|
|
|
FoldingRule CompositeExtractFeedingConstruct() {
|
|
// If the OpCompositeConstruct is simply putting back together elements that
|
|
// where extracted from the same souce, we can simlpy reuse the source.
|
|
//
|
|
// This is a common code pattern because of the way that scalar replacement
|
|
// works.
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>&) {
|
|
assert(inst->opcode() == SpvOpCompositeConstruct &&
|
|
"Wrong opcode. Should be OpCompositeConstruct.");
|
|
analysis::DefUseManager* def_use_mgr = inst->context()->get_def_use_mgr();
|
|
uint32_t original_id = 0;
|
|
|
|
// Check each element to make sure they are:
|
|
// - extractions
|
|
// - extracting the same position they are inserting
|
|
// - all extract from the same id.
|
|
for (uint32_t i = 0; i < inst->NumInOperands(); ++i) {
|
|
uint32_t element_id = inst->GetSingleWordInOperand(i);
|
|
ir::Instruction* element_inst = def_use_mgr->GetDef(element_id);
|
|
|
|
if (element_inst->opcode() != SpvOpCompositeExtract) {
|
|
return false;
|
|
}
|
|
|
|
if (element_inst->NumInOperands() != 2) {
|
|
return false;
|
|
}
|
|
|
|
if (element_inst->GetSingleWordInOperand(1) != i) {
|
|
return false;
|
|
}
|
|
|
|
if (i == 0) {
|
|
original_id =
|
|
element_inst->GetSingleWordInOperand(kExtractCompositeIdInIdx);
|
|
} else if (original_id != element_inst->GetSingleWordInOperand(
|
|
kExtractCompositeIdInIdx)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// The last check it to see that the object being extracted from is the
|
|
// correct type.
|
|
ir::Instruction* original_inst = def_use_mgr->GetDef(original_id);
|
|
if (original_inst->type_id() != inst->type_id()) {
|
|
return false;
|
|
}
|
|
|
|
// Simplify by using the original object.
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands({{SPV_OPERAND_TYPE_ID, {original_id}}});
|
|
return true;
|
|
};
|
|
}
|
|
|
|
FoldingRule InsertFeedingExtract() {
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>&) {
|
|
assert(inst->opcode() == SpvOpCompositeExtract &&
|
|
"Wrong opcode. Should be OpCompositeExtract.");
|
|
analysis::DefUseManager* def_use_mgr = inst->context()->get_def_use_mgr();
|
|
uint32_t cid = inst->GetSingleWordInOperand(kExtractCompositeIdInIdx);
|
|
ir::Instruction* cinst = def_use_mgr->GetDef(cid);
|
|
|
|
if (cinst->opcode() != SpvOpCompositeInsert) {
|
|
return false;
|
|
}
|
|
|
|
// Find the first position where the list of insert and extract indicies
|
|
// differ, if at all.
|
|
uint32_t i;
|
|
for (i = 1; i < inst->NumInOperands(); ++i) {
|
|
if (i + 1 >= cinst->NumInOperands()) {
|
|
break;
|
|
}
|
|
|
|
if (inst->GetSingleWordInOperand(i) !=
|
|
cinst->GetSingleWordInOperand(i + 1)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We are extracting the element that was inserted.
|
|
if (i == inst->NumInOperands() && i + 1 == cinst->NumInOperands()) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands(
|
|
{{SPV_OPERAND_TYPE_ID,
|
|
{cinst->GetSingleWordInOperand(kInsertObjectIdInIdx)}}});
|
|
return true;
|
|
}
|
|
|
|
// Extracting the value that was inserted along with values for the base
|
|
// composite. Cannot do anything.
|
|
if (i == inst->NumInOperands()) {
|
|
return false;
|
|
}
|
|
|
|
// Extracting an element of the value that was inserted. Extract from
|
|
// that value directly.
|
|
if (i + 1 == cinst->NumInOperands()) {
|
|
std::vector<ir::Operand> operands;
|
|
operands.push_back(
|
|
{SPV_OPERAND_TYPE_ID,
|
|
{cinst->GetSingleWordInOperand(kInsertObjectIdInIdx)}});
|
|
for (; i < inst->NumInOperands(); ++i) {
|
|
operands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
|
{cinst->GetSingleWordInOperand(i)}});
|
|
}
|
|
inst->SetInOperands(std::move(operands));
|
|
return true;
|
|
}
|
|
|
|
// Extracting a value that is disjoint from the element being inserted.
|
|
// Rewrite the extract to use the composite input to the insert.
|
|
std::vector<ir::Operand> operands;
|
|
operands.push_back(
|
|
{SPV_OPERAND_TYPE_ID,
|
|
{cinst->GetSingleWordInOperand(kInsertCompositeIdInIdx)}});
|
|
for (i = 1; i < inst->NumInOperands(); ++i) {
|
|
operands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
|
{inst->GetSingleWordInOperand(i)}});
|
|
}
|
|
inst->SetInOperands(std::move(operands));
|
|
return true;
|
|
};
|
|
}
|
|
|
|
FoldingRule RedundantPhi() {
|
|
// An OpPhi instruction where all values are the same or the result of the phi
|
|
// itself, can be replaced by the value itself.
|
|
return
|
|
[](ir::Instruction* inst, const std::vector<const analysis::Constant*>&) {
|
|
assert(inst->opcode() == SpvOpPhi && "Wrong opcode. Should be OpPhi.");
|
|
|
|
ir::IRContext* context = inst->context();
|
|
analysis::DefUseManager* def_use_mgr = context->get_def_use_mgr();
|
|
|
|
uint32_t incoming_value = 0;
|
|
|
|
for (uint32_t i = 0; i < inst->NumInOperands(); i += 2) {
|
|
uint32_t op_id = inst->GetSingleWordInOperand(i);
|
|
if (op_id == inst->result_id()) {
|
|
continue;
|
|
}
|
|
|
|
ir::Instruction* op_inst = def_use_mgr->GetDef(op_id);
|
|
if (op_inst->opcode() == SpvOpUndef) {
|
|
// TODO: We should be able to still use op_id if we know that
|
|
// the definition of op_id dominates |inst|.
|
|
return false;
|
|
}
|
|
|
|
if (incoming_value == 0) {
|
|
incoming_value = op_id;
|
|
} else if (op_id != incoming_value) {
|
|
// Found two possible value. Can't simplify.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (incoming_value == 0) {
|
|
// Code looks invalid. Don't do anything.
|
|
return false;
|
|
}
|
|
|
|
// We have a single incoming value. Simplify using that value.
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands({{SPV_OPERAND_TYPE_ID, {incoming_value}}});
|
|
return true;
|
|
};
|
|
}
|
|
|
|
FoldingRule RedundantSelect() {
|
|
// An OpSelect instruction where both values are the same or the condition is
|
|
// constant can be replaced by one of the values
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>& constants) {
|
|
assert(inst->opcode() == SpvOpSelect &&
|
|
"Wrong opcode. Should be OpSelect.");
|
|
assert(inst->NumInOperands() == 3);
|
|
assert(constants.size() == 3);
|
|
|
|
const analysis::BoolConstant* bc =
|
|
constants[0] ? constants[0]->AsBoolConstant() : nullptr;
|
|
uint32_t true_id = inst->GetSingleWordInOperand(1);
|
|
uint32_t false_id = inst->GetSingleWordInOperand(2);
|
|
|
|
if (bc) {
|
|
// Select condition is constant, result is known
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands(
|
|
{{SPV_OPERAND_TYPE_ID, {bc->value() ? true_id : false_id}}});
|
|
return true;
|
|
} else if (true_id == false_id) {
|
|
// Both results are the same, condition doesn't matter
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands({{SPV_OPERAND_TYPE_ID, {true_id}}});
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
}
|
|
|
|
enum class FloatConstantKind { Unknown, Zero, One };
|
|
|
|
FloatConstantKind getFloatConstantKind(const analysis::Constant* constant) {
|
|
if (constant == nullptr) {
|
|
return FloatConstantKind::Unknown;
|
|
}
|
|
|
|
if (const analysis::VectorConstant* vc = constant->AsVectorConstant()) {
|
|
const std::vector<const analysis::Constant*>& components =
|
|
vc->GetComponents();
|
|
assert(!components.empty());
|
|
|
|
FloatConstantKind kind = getFloatConstantKind(components[0]);
|
|
|
|
for (size_t i = 1; i < components.size(); ++i) {
|
|
if (getFloatConstantKind(components[i]) != kind) {
|
|
return FloatConstantKind::Unknown;
|
|
}
|
|
}
|
|
|
|
return kind;
|
|
} else if (const analysis::FloatConstant* fc = constant->AsFloatConstant()) {
|
|
double value = (fc->type()->AsFloat()->width() == 64) ? fc->GetDoubleValue()
|
|
: fc->GetFloatValue();
|
|
|
|
if (value == 0.0) {
|
|
return FloatConstantKind::Zero;
|
|
} else if (value == 1.0) {
|
|
return FloatConstantKind::One;
|
|
} else {
|
|
return FloatConstantKind::Unknown;
|
|
}
|
|
} else {
|
|
return FloatConstantKind::Unknown;
|
|
}
|
|
}
|
|
|
|
FoldingRule RedundantFAdd() {
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>& constants) {
|
|
assert(inst->opcode() == SpvOpFAdd && "Wrong opcode. Should be OpFAdd.");
|
|
assert(constants.size() == 2);
|
|
|
|
if (!inst->IsFloatingPointFoldingAllowed()) {
|
|
return false;
|
|
}
|
|
|
|
FloatConstantKind kind0 = getFloatConstantKind(constants[0]);
|
|
FloatConstantKind kind1 = getFloatConstantKind(constants[1]);
|
|
|
|
if (kind0 == FloatConstantKind::Zero || kind1 == FloatConstantKind::Zero) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands({{SPV_OPERAND_TYPE_ID,
|
|
{inst->GetSingleWordInOperand(
|
|
kind0 == FloatConstantKind::Zero ? 1 : 0)}}});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
FoldingRule RedundantFSub() {
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>& constants) {
|
|
assert(inst->opcode() == SpvOpFSub && "Wrong opcode. Should be OpFSub.");
|
|
assert(constants.size() == 2);
|
|
|
|
if (!inst->IsFloatingPointFoldingAllowed()) {
|
|
return false;
|
|
}
|
|
|
|
FloatConstantKind kind0 = getFloatConstantKind(constants[0]);
|
|
FloatConstantKind kind1 = getFloatConstantKind(constants[1]);
|
|
|
|
if (kind0 == FloatConstantKind::Zero) {
|
|
inst->SetOpcode(SpvOpFNegate);
|
|
inst->SetInOperands(
|
|
{{SPV_OPERAND_TYPE_ID, {inst->GetSingleWordInOperand(1)}}});
|
|
return true;
|
|
}
|
|
|
|
if (kind1 == FloatConstantKind::Zero) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands(
|
|
{{SPV_OPERAND_TYPE_ID, {inst->GetSingleWordInOperand(0)}}});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
FoldingRule RedundantFMul() {
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>& constants) {
|
|
assert(inst->opcode() == SpvOpFMul && "Wrong opcode. Should be OpFMul.");
|
|
assert(constants.size() == 2);
|
|
|
|
if (!inst->IsFloatingPointFoldingAllowed()) {
|
|
return false;
|
|
}
|
|
|
|
FloatConstantKind kind0 = getFloatConstantKind(constants[0]);
|
|
FloatConstantKind kind1 = getFloatConstantKind(constants[1]);
|
|
|
|
if (kind0 == FloatConstantKind::Zero || kind1 == FloatConstantKind::Zero) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands({{SPV_OPERAND_TYPE_ID,
|
|
{inst->GetSingleWordInOperand(
|
|
kind0 == FloatConstantKind::Zero ? 0 : 1)}}});
|
|
return true;
|
|
}
|
|
|
|
if (kind0 == FloatConstantKind::One || kind1 == FloatConstantKind::One) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands({{SPV_OPERAND_TYPE_ID,
|
|
{inst->GetSingleWordInOperand(
|
|
kind0 == FloatConstantKind::One ? 1 : 0)}}});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
FoldingRule RedundantFDiv() {
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>& constants) {
|
|
assert(inst->opcode() == SpvOpFDiv && "Wrong opcode. Should be OpFDiv.");
|
|
assert(constants.size() == 2);
|
|
|
|
if (!inst->IsFloatingPointFoldingAllowed()) {
|
|
return false;
|
|
}
|
|
|
|
FloatConstantKind kind0 = getFloatConstantKind(constants[0]);
|
|
FloatConstantKind kind1 = getFloatConstantKind(constants[1]);
|
|
|
|
if (kind0 == FloatConstantKind::Zero) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands(
|
|
{{SPV_OPERAND_TYPE_ID, {inst->GetSingleWordInOperand(0)}}});
|
|
return true;
|
|
}
|
|
|
|
if (kind1 == FloatConstantKind::One) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands(
|
|
{{SPV_OPERAND_TYPE_ID, {inst->GetSingleWordInOperand(0)}}});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
FoldingRule RedundantFMix() {
|
|
return [](ir::Instruction* inst,
|
|
const std::vector<const analysis::Constant*>& constants) {
|
|
assert(inst->opcode() == SpvOpExtInst &&
|
|
"Wrong opcode. Should be OpExtInst.");
|
|
|
|
if (!inst->IsFloatingPointFoldingAllowed()) {
|
|
return false;
|
|
}
|
|
|
|
uint32_t instSetId =
|
|
inst->context()->get_feature_mgr()->GetExtInstImportId_GLSLstd450();
|
|
|
|
if (inst->GetSingleWordInOperand(kExtInstSetIdInIdx) == instSetId &&
|
|
inst->GetSingleWordInOperand(kExtInstInstructionInIdx) ==
|
|
GLSLstd450FMix) {
|
|
assert(constants.size() == 5);
|
|
|
|
FloatConstantKind kind4 = getFloatConstantKind(constants[4]);
|
|
|
|
if (kind4 == FloatConstantKind::Zero || kind4 == FloatConstantKind::One) {
|
|
inst->SetOpcode(SpvOpCopyObject);
|
|
inst->SetInOperands(
|
|
{{SPV_OPERAND_TYPE_ID,
|
|
{inst->GetSingleWordInOperand(kind4 == FloatConstantKind::Zero
|
|
? kFMixXIdInIdx
|
|
: kFMixYIdInIdx)}}});
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
spvtools::opt::FoldingRules::FoldingRules() {
|
|
// Add all folding rules to the list for the opcodes to which they apply.
|
|
// Note that the order in which rules are added to the list matters. If a rule
|
|
// applies to the instruction, the rest of the rules will not be attempted.
|
|
// Take that into consideration.
|
|
|
|
rules_[SpvOpCompositeConstruct].push_back(CompositeExtractFeedingConstruct());
|
|
|
|
rules_[SpvOpCompositeExtract].push_back(InsertFeedingExtract());
|
|
rules_[SpvOpCompositeExtract].push_back(CompositeConstructFeedingExtract());
|
|
|
|
rules_[SpvOpExtInst].push_back(RedundantFMix());
|
|
|
|
rules_[SpvOpFAdd].push_back(RedundantFAdd());
|
|
rules_[SpvOpFDiv].push_back(RedundantFDiv());
|
|
rules_[SpvOpFMul].push_back(RedundantFMul());
|
|
rules_[SpvOpFSub].push_back(RedundantFSub());
|
|
|
|
rules_[SpvOpIMul].push_back(IntMultipleBy1());
|
|
|
|
rules_[SpvOpPhi].push_back(RedundantPhi());
|
|
|
|
rules_[SpvOpSelect].push_back(RedundantSelect());
|
|
}
|
|
|
|
} // namespace opt
|
|
} // namespace spvtools
|