SPIRV-Tools/source/opt/local_access_chain_convert_pass.cpp
2023-07-27 16:04:50 -07:00

472 lines
18 KiB
C++

// Copyright (c) 2017 The Khronos Group Inc.
// Copyright (c) 2017 Valve Corporation
// Copyright (c) 2017 LunarG Inc.
//
// 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/opt/local_access_chain_convert_pass.h"
#include "ir_context.h"
#include "iterator.h"
#include "source/util/string_utils.h"
namespace spvtools {
namespace opt {
namespace {
constexpr uint32_t kStoreValIdInIdx = 1;
constexpr uint32_t kAccessChainPtrIdInIdx = 0;
} // namespace
void LocalAccessChainConvertPass::BuildAndAppendInst(
spv::Op opcode, uint32_t typeId, uint32_t resultId,
const std::vector<Operand>& in_opnds,
std::vector<std::unique_ptr<Instruction>>* newInsts) {
std::unique_ptr<Instruction> newInst(
new Instruction(context(), opcode, typeId, resultId, in_opnds));
get_def_use_mgr()->AnalyzeInstDefUse(&*newInst);
newInsts->emplace_back(std::move(newInst));
}
uint32_t LocalAccessChainConvertPass::BuildAndAppendVarLoad(
const Instruction* ptrInst, uint32_t* varId, uint32_t* varPteTypeId,
std::vector<std::unique_ptr<Instruction>>* newInsts) {
const uint32_t ldResultId = TakeNextId();
if (ldResultId == 0) {
return 0;
}
*varId = ptrInst->GetSingleWordInOperand(kAccessChainPtrIdInIdx);
const Instruction* varInst = get_def_use_mgr()->GetDef(*varId);
assert(varInst->opcode() == spv::Op::OpVariable);
*varPteTypeId = GetPointeeTypeId(varInst);
BuildAndAppendInst(spv::Op::OpLoad, *varPteTypeId, ldResultId,
{{spv_operand_type_t::SPV_OPERAND_TYPE_ID, {*varId}}},
newInsts);
return ldResultId;
}
void LocalAccessChainConvertPass::AppendConstantOperands(
const Instruction* ptrInst, std::vector<Operand>* in_opnds) {
uint32_t iidIdx = 0;
ptrInst->ForEachInId([&iidIdx, &in_opnds, this](const uint32_t* iid) {
if (iidIdx > 0) {
const Instruction* cInst = get_def_use_mgr()->GetDef(*iid);
const auto* constant_value =
context()->get_constant_mgr()->GetConstantFromInst(cInst);
assert(constant_value != nullptr &&
"Expecting the index to be a constant.");
// We take the sign extended value because OpAccessChain interprets the
// index as signed.
int64_t long_value = constant_value->GetSignExtendedValue();
assert(long_value <= UINT32_MAX && long_value >= 0 &&
"The index value is too large for a composite insert or extract "
"instruction.");
uint32_t val = static_cast<uint32_t>(long_value);
in_opnds->push_back(
{spv_operand_type_t::SPV_OPERAND_TYPE_LITERAL_INTEGER, {val}});
}
++iidIdx;
});
}
bool LocalAccessChainConvertPass::ReplaceAccessChainLoad(
const Instruction* address_inst, Instruction* original_load) {
// Build and append load of variable in ptrInst
if (address_inst->NumInOperands() == 1) {
// An access chain with no indices is essentially a copy. All that is
// needed is to propagate the address.
context()->ReplaceAllUsesWith(
address_inst->result_id(),
address_inst->GetSingleWordInOperand(kAccessChainPtrIdInIdx));
return true;
}
std::vector<std::unique_ptr<Instruction>> new_inst;
uint32_t varId;
uint32_t varPteTypeId;
const uint32_t ldResultId =
BuildAndAppendVarLoad(address_inst, &varId, &varPteTypeId, &new_inst);
if (ldResultId == 0) {
return false;
}
new_inst[0]->UpdateDebugInfoFrom(original_load);
context()->get_decoration_mgr()->CloneDecorations(
original_load->result_id(), ldResultId,
{spv::Decoration::RelaxedPrecision});
original_load->InsertBefore(std::move(new_inst));
context()->get_debug_info_mgr()->AnalyzeDebugInst(
original_load->PreviousNode());
// Rewrite |original_load| into an extract.
Instruction::OperandList new_operands;
// copy the result id and the type id to the new operand list.
new_operands.emplace_back(original_load->GetOperand(0));
new_operands.emplace_back(original_load->GetOperand(1));
new_operands.emplace_back(
Operand({spv_operand_type_t::SPV_OPERAND_TYPE_ID, {ldResultId}}));
AppendConstantOperands(address_inst, &new_operands);
original_load->SetOpcode(spv::Op::OpCompositeExtract);
original_load->ReplaceOperands(new_operands);
context()->UpdateDefUse(original_load);
return true;
}
bool LocalAccessChainConvertPass::GenAccessChainStoreReplacement(
const Instruction* ptrInst, uint32_t valId,
std::vector<std::unique_ptr<Instruction>>* newInsts) {
if (ptrInst->NumInOperands() == 1) {
// An access chain with no indices is essentially a copy. However, we still
// have to create a new store because the old ones will be deleted.
BuildAndAppendInst(
spv::Op::OpStore, 0, 0,
{{spv_operand_type_t::SPV_OPERAND_TYPE_ID,
{ptrInst->GetSingleWordInOperand(kAccessChainPtrIdInIdx)}},
{spv_operand_type_t::SPV_OPERAND_TYPE_ID, {valId}}},
newInsts);
return true;
}
// Build and append load of variable in ptrInst
uint32_t varId;
uint32_t varPteTypeId;
const uint32_t ldResultId =
BuildAndAppendVarLoad(ptrInst, &varId, &varPteTypeId, newInsts);
if (ldResultId == 0) {
return false;
}
context()->get_decoration_mgr()->CloneDecorations(
varId, ldResultId, {spv::Decoration::RelaxedPrecision});
// Build and append Insert
const uint32_t insResultId = TakeNextId();
if (insResultId == 0) {
return false;
}
std::vector<Operand> ins_in_opnds = {
{spv_operand_type_t::SPV_OPERAND_TYPE_ID, {valId}},
{spv_operand_type_t::SPV_OPERAND_TYPE_ID, {ldResultId}}};
AppendConstantOperands(ptrInst, &ins_in_opnds);
BuildAndAppendInst(spv::Op::OpCompositeInsert, varPteTypeId, insResultId,
ins_in_opnds, newInsts);
context()->get_decoration_mgr()->CloneDecorations(
varId, insResultId, {spv::Decoration::RelaxedPrecision});
// Build and append Store
BuildAndAppendInst(spv::Op::OpStore, 0, 0,
{{spv_operand_type_t::SPV_OPERAND_TYPE_ID, {varId}},
{spv_operand_type_t::SPV_OPERAND_TYPE_ID, {insResultId}}},
newInsts);
return true;
}
bool LocalAccessChainConvertPass::Is32BitConstantIndexAccessChain(
const Instruction* acp) const {
uint32_t inIdx = 0;
return acp->WhileEachInId([&inIdx, this](const uint32_t* tid) {
if (inIdx > 0) {
Instruction* opInst = get_def_use_mgr()->GetDef(*tid);
if (opInst->opcode() != spv::Op::OpConstant) return false;
const auto* index =
context()->get_constant_mgr()->GetConstantFromInst(opInst);
int64_t index_value = index->GetSignExtendedValue();
if (index_value > UINT32_MAX) return false;
if (index_value < 0) return false;
}
++inIdx;
return true;
});
}
bool LocalAccessChainConvertPass::HasOnlySupportedRefs(uint32_t ptrId) {
if (supported_ref_ptrs_.find(ptrId) != supported_ref_ptrs_.end()) return true;
if (get_def_use_mgr()->WhileEachUser(ptrId, [this](Instruction* user) {
if (user->GetCommonDebugOpcode() == CommonDebugInfoDebugValue ||
user->GetCommonDebugOpcode() == CommonDebugInfoDebugDeclare) {
return true;
}
spv::Op op = user->opcode();
if (IsNonPtrAccessChain(op) || op == spv::Op::OpCopyObject) {
if (!HasOnlySupportedRefs(user->result_id())) {
return false;
}
} else if (op != spv::Op::OpStore && op != spv::Op::OpLoad &&
op != spv::Op::OpName && !IsNonTypeDecorate(op)) {
return false;
}
return true;
})) {
supported_ref_ptrs_.insert(ptrId);
return true;
}
return false;
}
void LocalAccessChainConvertPass::FindTargetVars(Function* func) {
for (auto bi = func->begin(); bi != func->end(); ++bi) {
for (auto ii = bi->begin(); ii != bi->end(); ++ii) {
switch (ii->opcode()) {
case spv::Op::OpStore:
case spv::Op::OpLoad: {
uint32_t varId;
Instruction* ptrInst = GetPtr(&*ii, &varId);
if (!IsTargetVar(varId)) break;
const spv::Op op = ptrInst->opcode();
// Rule out variables with non-supported refs eg function calls
if (!HasOnlySupportedRefs(varId)) {
seen_non_target_vars_.insert(varId);
seen_target_vars_.erase(varId);
break;
}
// Rule out variables with nested access chains
// TODO(): Convert nested access chains
bool is_non_ptr_access_chain = IsNonPtrAccessChain(op);
if (is_non_ptr_access_chain && ptrInst->GetSingleWordInOperand(
kAccessChainPtrIdInIdx) != varId) {
seen_non_target_vars_.insert(varId);
seen_target_vars_.erase(varId);
break;
}
// Rule out variables accessed with non-constant indices
if (!Is32BitConstantIndexAccessChain(ptrInst)) {
seen_non_target_vars_.insert(varId);
seen_target_vars_.erase(varId);
break;
}
if (is_non_ptr_access_chain && AnyIndexIsOutOfBounds(ptrInst)) {
seen_non_target_vars_.insert(varId);
seen_target_vars_.erase(varId);
break;
}
} break;
default:
break;
}
}
}
}
Pass::Status LocalAccessChainConvertPass::ConvertLocalAccessChains(
Function* func) {
FindTargetVars(func);
// Replace access chains of all targeted variables with equivalent
// extract and insert sequences
bool modified = false;
for (auto bi = func->begin(); bi != func->end(); ++bi) {
std::vector<Instruction*> dead_instructions;
for (auto ii = bi->begin(); ii != bi->end(); ++ii) {
switch (ii->opcode()) {
case spv::Op::OpLoad: {
uint32_t varId;
Instruction* ptrInst = GetPtr(&*ii, &varId);
if (!IsNonPtrAccessChain(ptrInst->opcode())) break;
if (!IsTargetVar(varId)) break;
if (!ReplaceAccessChainLoad(ptrInst, &*ii)) {
return Status::Failure;
}
modified = true;
} break;
case spv::Op::OpStore: {
uint32_t varId;
Instruction* store = &*ii;
Instruction* ptrInst = GetPtr(store, &varId);
if (!IsNonPtrAccessChain(ptrInst->opcode())) break;
if (!IsTargetVar(varId)) break;
std::vector<std::unique_ptr<Instruction>> newInsts;
uint32_t valId = store->GetSingleWordInOperand(kStoreValIdInIdx);
if (!GenAccessChainStoreReplacement(ptrInst, valId, &newInsts)) {
return Status::Failure;
}
size_t num_of_instructions_to_skip = newInsts.size() - 1;
dead_instructions.push_back(store);
++ii;
ii = ii.InsertBefore(std::move(newInsts));
for (size_t i = 0; i < num_of_instructions_to_skip; ++i) {
ii->UpdateDebugInfoFrom(store);
context()->get_debug_info_mgr()->AnalyzeDebugInst(&*ii);
++ii;
}
ii->UpdateDebugInfoFrom(store);
context()->get_debug_info_mgr()->AnalyzeDebugInst(&*ii);
modified = true;
} break;
default:
break;
}
}
while (!dead_instructions.empty()) {
Instruction* inst = dead_instructions.back();
dead_instructions.pop_back();
DCEInst(inst, [&dead_instructions](Instruction* other_inst) {
auto i = std::find(dead_instructions.begin(), dead_instructions.end(),
other_inst);
if (i != dead_instructions.end()) {
dead_instructions.erase(i);
}
});
}
}
return (modified ? Status::SuccessWithChange : Status::SuccessWithoutChange);
}
void LocalAccessChainConvertPass::Initialize() {
// Initialize Target Variable Caches
seen_target_vars_.clear();
seen_non_target_vars_.clear();
// Initialize collections
supported_ref_ptrs_.clear();
// Initialize extension allowlist
InitExtensions();
}
bool LocalAccessChainConvertPass::AllExtensionsSupported() const {
// This capability can now exist without the extension, so we have to check
// for the capability. This pass is only looking at function scope symbols,
// so we do not care if there are variable pointers on storage buffers.
if (context()->get_feature_mgr()->HasCapability(
spv::Capability::VariablePointers))
return false;
// If any extension not in allowlist, return false
for (auto& ei : get_module()->extensions()) {
const std::string extName = ei.GetInOperand(0).AsString();
if (extensions_allowlist_.find(extName) == extensions_allowlist_.end())
return false;
}
// only allow NonSemantic.Shader.DebugInfo.100, we cannot safely optimise
// around unknown extended
// instruction sets even if they are non-semantic
for (auto& inst : context()->module()->ext_inst_imports()) {
assert(inst.opcode() == spv::Op::OpExtInstImport &&
"Expecting an import of an extension's instruction set.");
const std::string extension_name = inst.GetInOperand(0).AsString();
if (spvtools::utils::starts_with(extension_name, "NonSemantic.") &&
extension_name != "NonSemantic.Shader.DebugInfo.100") {
return false;
}
}
return true;
}
Pass::Status LocalAccessChainConvertPass::ProcessImpl() {
// Do not process if module contains OpGroupDecorate. Additional
// support required in KillNamesAndDecorates().
// TODO(greg-lunarg): Add support for OpGroupDecorate
for (auto& ai : get_module()->annotations())
if (ai.opcode() == spv::Op::OpGroupDecorate)
return Status::SuccessWithoutChange;
// Do not process if any disallowed extensions are enabled
if (!AllExtensionsSupported()) return Status::SuccessWithoutChange;
// Process all functions in the module.
Status status = Status::SuccessWithoutChange;
for (Function& func : *get_module()) {
status = CombineStatus(status, ConvertLocalAccessChains(&func));
if (status == Status::Failure) {
break;
}
}
return status;
}
LocalAccessChainConvertPass::LocalAccessChainConvertPass() {}
Pass::Status LocalAccessChainConvertPass::Process() {
Initialize();
return ProcessImpl();
}
void LocalAccessChainConvertPass::InitExtensions() {
extensions_allowlist_.clear();
extensions_allowlist_.insert(
{"SPV_AMD_shader_explicit_vertex_parameter",
"SPV_AMD_shader_trinary_minmax", "SPV_AMD_gcn_shader",
"SPV_KHR_shader_ballot", "SPV_AMD_shader_ballot",
"SPV_AMD_gpu_shader_half_float", "SPV_KHR_shader_draw_parameters",
"SPV_KHR_subgroup_vote", "SPV_KHR_8bit_storage", "SPV_KHR_16bit_storage",
"SPV_KHR_device_group", "SPV_KHR_multiview",
"SPV_NVX_multiview_per_view_attributes", "SPV_NV_viewport_array2",
"SPV_NV_stereo_view_rendering", "SPV_NV_sample_mask_override_coverage",
"SPV_NV_geometry_shader_passthrough", "SPV_AMD_texture_gather_bias_lod",
"SPV_KHR_storage_buffer_storage_class",
// SPV_KHR_variable_pointers
// Currently do not support extended pointer expressions
"SPV_AMD_gpu_shader_int16", "SPV_KHR_post_depth_coverage",
"SPV_KHR_shader_atomic_counter_ops", "SPV_EXT_shader_stencil_export",
"SPV_EXT_shader_viewport_index_layer",
"SPV_AMD_shader_image_load_store_lod", "SPV_AMD_shader_fragment_mask",
"SPV_EXT_fragment_fully_covered", "SPV_AMD_gpu_shader_half_float_fetch",
"SPV_GOOGLE_decorate_string", "SPV_GOOGLE_hlsl_functionality1",
"SPV_GOOGLE_user_type", "SPV_NV_shader_subgroup_partitioned",
"SPV_EXT_demote_to_helper_invocation", "SPV_EXT_descriptor_indexing",
"SPV_NV_fragment_shader_barycentric",
"SPV_NV_compute_shader_derivatives", "SPV_NV_shader_image_footprint",
"SPV_NV_shading_rate", "SPV_NV_mesh_shader", "SPV_NV_ray_tracing",
"SPV_KHR_ray_tracing", "SPV_KHR_ray_query",
"SPV_EXT_fragment_invocation_density", "SPV_KHR_terminate_invocation",
"SPV_KHR_subgroup_uniform_control_flow", "SPV_KHR_integer_dot_product",
"SPV_EXT_shader_image_int64", "SPV_KHR_non_semantic_info",
"SPV_KHR_uniform_group_instructions",
"SPV_KHR_fragment_shader_barycentric", "SPV_KHR_vulkan_memory_model",
"SPV_NV_bindless_texture", "SPV_EXT_shader_atomic_float_add"});
}
bool LocalAccessChainConvertPass::AnyIndexIsOutOfBounds(
const Instruction* access_chain_inst) {
assert(IsNonPtrAccessChain(access_chain_inst->opcode()));
analysis::TypeManager* type_mgr = context()->get_type_mgr();
analysis::ConstantManager* const_mgr = context()->get_constant_mgr();
auto constants = const_mgr->GetOperandConstants(access_chain_inst);
uint32_t base_pointer_id = access_chain_inst->GetSingleWordInOperand(0);
Instruction* base_pointer = get_def_use_mgr()->GetDef(base_pointer_id);
const analysis::Pointer* base_pointer_type =
type_mgr->GetType(base_pointer->type_id())->AsPointer();
assert(base_pointer_type != nullptr &&
"The base of the access chain is not a pointer.");
const analysis::Type* current_type = base_pointer_type->pointee_type();
for (uint32_t i = 1; i < access_chain_inst->NumInOperands(); ++i) {
if (IsIndexOutOfBounds(constants[i], current_type)) {
return true;
}
uint32_t index =
(constants[i]
? static_cast<uint32_t>(constants[i]->GetZeroExtendedValue())
: 0);
current_type = type_mgr->GetMemberType(current_type, {index});
}
return false;
}
bool LocalAccessChainConvertPass::IsIndexOutOfBounds(
const analysis::Constant* index, const analysis::Type* type) const {
if (index == nullptr) {
return false;
}
return index->GetZeroExtendedValue() >= type->NumberOfComponents();
}
} // namespace opt
} // namespace spvtools