mirror of
https://github.com/KhronosGroup/SPIRV-Tools
synced 2024-12-29 11:11:06 +00:00
31590104ec
spirv-opt: Add --graphics-robust-access Clamps access chain indices so they are always in bounds. Assumes: - Logical addressing mode - No runtime-array-descriptor-indexing - No variable pointers Adds stub code for clamping coordinate and samples for OpImageTexelPointer. Adds SinglePassRunAndFail optimizer test fixture. Android.mk: add source/opt/graphics_robust_access_pass.cpp Adds Constant::GetSignExtendedValue, Constant::GetZeroExtendedValue
969 lines
41 KiB
C++
969 lines
41 KiB
C++
// Copyright (c) 2019 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.
|
|
|
|
// This pass injects code in a graphics shader to implement guarantees
|
|
// satisfying Vulkan's robustBufferAcces rules. Robust access rules permit
|
|
// an out-of-bounds access to be redirected to an access of the same type
|
|
// (load, store, etc.) but within the same root object.
|
|
//
|
|
// We assume baseline functionality in Vulkan, i.e. the module uses
|
|
// logical addressing mode, without VK_KHR_variable_pointers.
|
|
//
|
|
// - Logical addressing mode implies:
|
|
// - Each root pointer (a pointer that exists other than by the
|
|
// execution of a shader instruction) is the result of an OpVariable.
|
|
//
|
|
// - Instructions that result in pointers are:
|
|
// OpVariable
|
|
// OpAccessChain
|
|
// OpInBoundsAccessChain
|
|
// OpFunctionParameter
|
|
// OpImageTexelPointer
|
|
// OpCopyObject
|
|
//
|
|
// - Instructions that use a pointer are:
|
|
// OpLoad
|
|
// OpStore
|
|
// OpAccessChain
|
|
// OpInBoundsAccessChain
|
|
// OpFunctionCall
|
|
// OpImageTexelPointer
|
|
// OpCopyMemory
|
|
// OpCopyObject
|
|
// all OpAtomic* instructions
|
|
//
|
|
// We classify pointer-users into:
|
|
// - Accesses:
|
|
// - OpLoad
|
|
// - OpStore
|
|
// - OpAtomic*
|
|
// - OpCopyMemory
|
|
//
|
|
// - Address calculations:
|
|
// - OpAccessChain
|
|
// - OpInBoundsAccessChain
|
|
//
|
|
// - Pass-through:
|
|
// - OpFunctionCall
|
|
// - OpFunctionParameter
|
|
// - OpCopyObject
|
|
//
|
|
// The strategy is:
|
|
//
|
|
// - Handle only logical addressing mode. In particular, don't handle a module
|
|
// if it uses one of the variable-pointers capabilities.
|
|
//
|
|
// - Don't handle modules using capability RuntimeDescriptorArrayEXT. So the
|
|
// only runtime arrays are those that are the last member in a
|
|
// Block-decorated struct. This allows us to feasibly/easily compute the
|
|
// length of the runtime array. See below.
|
|
//
|
|
// - The memory locations accessed by OpLoad, OpStore, OpCopyMemory, and
|
|
// OpAtomic* are determined by their pointer parameter or parameters.
|
|
// Pointers are always (correctly) typed and so the address and number of
|
|
// consecutive locations are fully determined by the pointer.
|
|
//
|
|
// - A pointer value orginates as one of few cases:
|
|
//
|
|
// - OpVariable for an interface object or an array of them: image,
|
|
// buffer (UBO or SSBO), sampler, sampled-image, push-constant, input
|
|
// variable, output variable. The execution environment is responsible for
|
|
// allocating the correct amount of storage for these, and for ensuring
|
|
// each resource bound to such a variable is big enough to contain the
|
|
// SPIR-V pointee type of the variable.
|
|
//
|
|
// - OpVariable for a non-interface object. These are variables in
|
|
// Workgroup, Private, and Function storage classes. The compiler ensures
|
|
// the underlying allocation is big enough to store the entire SPIR-V
|
|
// pointee type of the variable.
|
|
//
|
|
// - An OpFunctionParameter. This always maps to a pointer parameter to an
|
|
// OpFunctionCall.
|
|
//
|
|
// - In logical addressing mode, these are severely limited:
|
|
// "Any pointer operand to an OpFunctionCall must be:
|
|
// - a memory object declaration, or
|
|
// - a pointer to an element in an array that is a memory object
|
|
// declaration, where the element type is OpTypeSampler or OpTypeImage"
|
|
//
|
|
// - This has an important simplifying consequence:
|
|
//
|
|
// - When looking for a pointer to the structure containing a runtime
|
|
// array, you begin with a pointer to the runtime array and trace
|
|
// backward in the function. You never have to trace back beyond
|
|
// your function call boundary. So you can't take a partial access
|
|
// chain into an SSBO, then pass that pointer into a function. So
|
|
// we don't resort to using fat pointers to compute array length.
|
|
// We can trace back to a pointer to the containing structure,
|
|
// and use that in an OpArrayLength instruction. (The structure type
|
|
// gives us the member index of the runtime array.)
|
|
//
|
|
// - Otherwise, the pointer type fully encodes the range of valid
|
|
// addresses. In particular, the type of a pointer to an aggregate
|
|
// value fully encodes the range of indices when indexing into
|
|
// that aggregate.
|
|
//
|
|
// - The pointer is the result of an access chain instruction. We clamp
|
|
// indices contributing to address calculations. As noted above, the
|
|
// valid ranges are either bound by the length of a runtime array, or
|
|
// by the type of the base pointer. The length of a runtime array is
|
|
// the result of an OpArrayLength instruction acting on the pointer of
|
|
// the containing structure as noted above.
|
|
//
|
|
// - TODO(dneto): OpImageTexelPointer:
|
|
// - Clamp coordinate to the image size returned by OpImageQuerySize
|
|
// - If multi-sampled, clamp the sample index to the count returned by
|
|
// OpImageQuerySamples.
|
|
// - If not multi-sampled, set the sample index to 0.
|
|
//
|
|
// - Rely on the external validator to check that pointers are only
|
|
// used by the instructions as above.
|
|
//
|
|
// - Handles OpTypeRuntimeArray
|
|
// Track pointer back to original resource (pointer to struct), so we can
|
|
// query the runtime array size.
|
|
//
|
|
|
|
#include "graphics_robust_access_pass.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <functional>
|
|
#include <initializer_list>
|
|
#include <utility>
|
|
|
|
#include "constants.h"
|
|
#include "def_use_manager.h"
|
|
#include "function.h"
|
|
#include "ir_context.h"
|
|
#include "module.h"
|
|
#include "pass.h"
|
|
#include "source/diagnostic.h"
|
|
#include "source/util/make_unique.h"
|
|
#include "spirv-tools/libspirv.h"
|
|
#include "spirv/unified1/GLSL.std.450.h"
|
|
#include "spirv/unified1/spirv.h"
|
|
#include "type_manager.h"
|
|
#include "types.h"
|
|
|
|
namespace spvtools {
|
|
namespace opt {
|
|
|
|
using opt::BasicBlock;
|
|
using opt::Instruction;
|
|
using opt::Operand;
|
|
using spvtools::MakeUnique;
|
|
|
|
GraphicsRobustAccessPass::GraphicsRobustAccessPass() : module_status_() {}
|
|
|
|
Pass::Status GraphicsRobustAccessPass::Process() {
|
|
module_status_ = PerModuleState();
|
|
|
|
ProcessCurrentModule();
|
|
|
|
auto result = module_status_.failed
|
|
? Status::Failure
|
|
: (module_status_.modified ? Status::SuccessWithChange
|
|
: Status::SuccessWithoutChange);
|
|
|
|
return result;
|
|
}
|
|
|
|
spvtools::DiagnosticStream GraphicsRobustAccessPass::Fail() {
|
|
module_status_.failed = true;
|
|
// We don't really have a position, and we'll ignore the result.
|
|
return std::move(
|
|
spvtools::DiagnosticStream({}, consumer(), "", SPV_ERROR_INVALID_BINARY)
|
|
<< name() << ": ");
|
|
}
|
|
|
|
spv_result_t GraphicsRobustAccessPass::IsCompatibleModule() {
|
|
auto* feature_mgr = context()->get_feature_mgr();
|
|
if (!feature_mgr->HasCapability(SpvCapabilityShader))
|
|
return Fail() << "Can only process Shader modules";
|
|
if (feature_mgr->HasCapability(SpvCapabilityVariablePointers))
|
|
return Fail() << "Can't process modules with VariablePointers capability";
|
|
if (feature_mgr->HasCapability(SpvCapabilityVariablePointersStorageBuffer))
|
|
return Fail() << "Can't process modules with VariablePointersStorageBuffer "
|
|
"capability";
|
|
if (feature_mgr->HasCapability(SpvCapabilityRuntimeDescriptorArrayEXT)) {
|
|
// These have a RuntimeArray outside of Block-decorated struct. There
|
|
// is no way to compute the array length from within SPIR-V.
|
|
return Fail() << "Can't process modules with RuntimeDescriptorArrayEXT "
|
|
"capability";
|
|
}
|
|
|
|
{
|
|
auto* inst = context()->module()->GetMemoryModel();
|
|
const auto addressing_model = inst->GetSingleWordOperand(0);
|
|
if (addressing_model != SpvAddressingModelLogical)
|
|
return Fail() << "Addressing model must be Logical. Found "
|
|
<< inst->PrettyPrint();
|
|
}
|
|
return SPV_SUCCESS;
|
|
}
|
|
|
|
spv_result_t GraphicsRobustAccessPass::ProcessCurrentModule() {
|
|
auto err = IsCompatibleModule();
|
|
if (err != SPV_SUCCESS) return err;
|
|
|
|
ProcessFunction fn = [this](opt::Function* f) { return ProcessAFunction(f); };
|
|
module_status_.modified |= context()->ProcessReachableCallTree(fn);
|
|
|
|
// Need something here. It's the price we pay for easier failure paths.
|
|
return SPV_SUCCESS;
|
|
}
|
|
|
|
bool GraphicsRobustAccessPass::ProcessAFunction(opt::Function* function) {
|
|
// Ensure that all pointers computed inside a function are within bounds.
|
|
// Find the access chains in this block before trying to modify them.
|
|
std::vector<Instruction*> access_chains;
|
|
std::vector<Instruction*> image_texel_pointers;
|
|
for (auto& block : *function) {
|
|
for (auto& inst : block) {
|
|
switch (inst.opcode()) {
|
|
case SpvOpAccessChain:
|
|
case SpvOpInBoundsAccessChain:
|
|
access_chains.push_back(&inst);
|
|
break;
|
|
case SpvOpImageTexelPointer:
|
|
image_texel_pointers.push_back(&inst);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
for (auto* inst : access_chains) {
|
|
ClampIndicesForAccessChain(inst);
|
|
}
|
|
|
|
for (auto* inst : image_texel_pointers) {
|
|
if (SPV_SUCCESS != ClampCoordinateForImageTexelPointer(inst)) break;
|
|
}
|
|
return module_status_.modified;
|
|
}
|
|
|
|
void GraphicsRobustAccessPass::ClampIndicesForAccessChain(
|
|
Instruction* access_chain) {
|
|
Instruction& inst = *access_chain;
|
|
|
|
auto* constant_mgr = context()->get_constant_mgr();
|
|
auto* def_use_mgr = context()->get_def_use_mgr();
|
|
auto* type_mgr = context()->get_type_mgr();
|
|
|
|
// Replaces one of the OpAccessChain index operands with a new value.
|
|
// Updates def-use analysis.
|
|
auto replace_index = [&inst, def_use_mgr](uint32_t operand_index,
|
|
Instruction* new_value) {
|
|
inst.SetOperand(operand_index, {new_value->result_id()});
|
|
def_use_mgr->AnalyzeInstUse(&inst);
|
|
};
|
|
|
|
// Replaces one of the OpAccesssChain index operands with a clamped value.
|
|
// Replace the operand at |operand_index| with the value computed from
|
|
// unsigned_clamp(%old_value, %min_value, %max_value). It also analyzes
|
|
// the new instruction and records that them module is modified.
|
|
auto clamp_index = [&inst, this, &replace_index](
|
|
uint32_t operand_index, Instruction* old_value,
|
|
Instruction* min_value, Instruction* max_value) {
|
|
auto* clamp_inst = MakeClampInst(old_value, min_value, max_value, &inst);
|
|
replace_index(operand_index, clamp_inst);
|
|
};
|
|
|
|
// Ensures the specified index of access chain |inst| has a value that is
|
|
// at most |count| - 1. If the index is already a constant value less than
|
|
// |count| then no change is made.
|
|
auto clamp_to_literal_count = [&inst, this, &constant_mgr, &type_mgr,
|
|
&replace_index, &clamp_index](
|
|
uint32_t operand_index, uint64_t count) {
|
|
Instruction* index_inst =
|
|
this->GetDef(inst.GetSingleWordOperand(operand_index));
|
|
const auto* index_type =
|
|
type_mgr->GetType(index_inst->type_id())->AsInteger();
|
|
assert(index_type);
|
|
if (count <= 1) {
|
|
// Replace the index with 0.
|
|
replace_index(operand_index, GetValueForType(0, index_type));
|
|
return;
|
|
}
|
|
|
|
const auto index_width = index_type->width();
|
|
|
|
// If the index is a constant then |index_constant| will not be a null
|
|
// pointer. (If index is an |OpConstantNull| then it |index_constant| will
|
|
// not be a null pointer.) Since access chain indices must be scalar
|
|
// integers, this can't be a spec constant.
|
|
if (auto* index_constant = constant_mgr->GetConstantFromInst(index_inst)) {
|
|
auto* int_index_constant = index_constant->AsIntConstant();
|
|
int64_t value = 0;
|
|
// OpAccessChain indices are treated as signed. So get the signed
|
|
// constant value here.
|
|
if (index_width <= 32) {
|
|
value = int64_t(int_index_constant->GetS32BitValue());
|
|
} else if (index_width <= 64) {
|
|
value = int_index_constant->GetS64BitValue();
|
|
} else {
|
|
this->Fail() << "Can't handle indices wider than 64 bits, found "
|
|
"constant index with "
|
|
<< index_type->width() << "bits";
|
|
return;
|
|
}
|
|
if (value < 0) {
|
|
replace_index(operand_index, GetValueForType(0, index_type));
|
|
} else if (uint64_t(value) < count) {
|
|
// Nothing to do.
|
|
return;
|
|
} else {
|
|
// Replace with count - 1.
|
|
assert(count > 0); // Already took care of this case above.
|
|
replace_index(operand_index, GetValueForType(count - 1, index_type));
|
|
}
|
|
} else {
|
|
// Generate a clamp instruction.
|
|
|
|
// Compute the bit width of a viable type to hold (count-1).
|
|
const auto maxval = count - 1;
|
|
const auto* maxval_type = index_type;
|
|
// Look for a bit width, up to 64 bits wide, to fit maxval.
|
|
uint32_t maxval_width = index_width;
|
|
while ((maxval_width < 64) && (0 != (maxval >> maxval_width))) {
|
|
maxval_width *= 2;
|
|
}
|
|
// Widen the index value if necessary
|
|
if (maxval_width > index_width) {
|
|
// Find the wider type. We only need this case if a constant (array)
|
|
// bound is too big. This never requires us to *add* a capability
|
|
// declaration for Int64 because the existence of the array bound would
|
|
// already have required that declaration.
|
|
index_inst = WidenInteger(index_type->IsSigned(), maxval_width,
|
|
index_inst, &inst);
|
|
maxval_type = type_mgr->GetType(index_inst->type_id())->AsInteger();
|
|
}
|
|
// Finally, clamp the index.
|
|
clamp_index(operand_index, index_inst, GetValueForType(0, maxval_type),
|
|
GetValueForType(maxval, maxval_type));
|
|
}
|
|
};
|
|
|
|
// Ensures the specified index of access chain |inst| has a value that is at
|
|
// most the value of |count_inst| minus 1, where |count_inst| is treated as an
|
|
// unsigned integer.
|
|
auto clamp_to_count = [&inst, this, &constant_mgr, &clamp_to_literal_count,
|
|
&clamp_index, &type_mgr](uint32_t operand_index,
|
|
Instruction* count_inst) {
|
|
Instruction* index_inst =
|
|
this->GetDef(inst.GetSingleWordOperand(operand_index));
|
|
const auto* index_type =
|
|
type_mgr->GetType(index_inst->type_id())->AsInteger();
|
|
const auto* count_type =
|
|
type_mgr->GetType(count_inst->type_id())->AsInteger();
|
|
assert(index_type);
|
|
if (const auto* count_constant =
|
|
constant_mgr->GetConstantFromInst(count_inst)) {
|
|
uint64_t value = 0;
|
|
const auto width = count_constant->type()->AsInteger()->width();
|
|
if (width <= 32) {
|
|
value = count_constant->AsIntConstant()->GetU32BitValue();
|
|
} else if (width <= 64) {
|
|
value = count_constant->AsIntConstant()->GetU64BitValue();
|
|
} else {
|
|
this->Fail() << "Can't handle indices wider than 64 bits, found "
|
|
"constant index with "
|
|
<< index_type->width() << "bits";
|
|
return;
|
|
}
|
|
clamp_to_literal_count(operand_index, value);
|
|
} else {
|
|
// Widen them to the same width.
|
|
const auto index_width = index_type->width();
|
|
const auto count_width = count_type->width();
|
|
const auto target_width = std::max(index_width, count_width);
|
|
// UConvert requires the result type to have 0 signedness. So enforce
|
|
// that here.
|
|
auto* wider_type = index_width < count_width ? count_type : index_type;
|
|
if (index_type->width() < target_width) {
|
|
// Access chain indices are treated as signed integers.
|
|
index_inst = WidenInteger(true, target_width, index_inst, &inst);
|
|
} else if (count_type->width() < target_width) {
|
|
// Assume type sizes are treated as unsigned.
|
|
count_inst = WidenInteger(false, target_width, count_inst, &inst);
|
|
}
|
|
// Compute count - 1.
|
|
// It doesn't matter if 1 is signed or unsigned.
|
|
auto* one = GetValueForType(1, wider_type);
|
|
auto* count_minus_1 = InsertInst(
|
|
&inst, SpvOpISub, type_mgr->GetId(wider_type), TakeNextId(),
|
|
{{SPV_OPERAND_TYPE_ID, {count_inst->result_id()}},
|
|
{SPV_OPERAND_TYPE_ID, {one->result_id()}}});
|
|
clamp_index(operand_index, index_inst, GetValueForType(0, wider_type),
|
|
count_minus_1);
|
|
}
|
|
};
|
|
|
|
const Instruction* base_inst = GetDef(inst.GetSingleWordInOperand(0));
|
|
const Instruction* base_type = GetDef(base_inst->type_id());
|
|
Instruction* pointee_type = GetDef(base_type->GetSingleWordInOperand(1));
|
|
|
|
// Walk the indices from earliest to latest, replacing indices with a
|
|
// clamped value, and updating the pointee_type. The order matters for
|
|
// the case when we have to compute the length of a runtime array. In
|
|
// that the algorithm relies on the fact that that the earlier indices
|
|
// have already been clamped.
|
|
const uint32_t num_operands = inst.NumOperands();
|
|
for (uint32_t idx = 3; !module_status_.failed && idx < num_operands; ++idx) {
|
|
const uint32_t index_id = inst.GetSingleWordOperand(idx);
|
|
Instruction* index_inst = GetDef(index_id);
|
|
|
|
switch (pointee_type->opcode()) {
|
|
case SpvOpTypeMatrix: // Use column count
|
|
case SpvOpTypeVector: // Use component count
|
|
{
|
|
const uint32_t count = pointee_type->GetSingleWordOperand(2);
|
|
clamp_to_literal_count(idx, count);
|
|
pointee_type = GetDef(pointee_type->GetSingleWordOperand(1));
|
|
} break;
|
|
|
|
case SpvOpTypeArray: {
|
|
// The array length can be a spec constant, so go through the general
|
|
// case.
|
|
Instruction* array_len = GetDef(pointee_type->GetSingleWordOperand(2));
|
|
clamp_to_count(idx, array_len);
|
|
pointee_type = GetDef(pointee_type->GetSingleWordOperand(1));
|
|
} break;
|
|
|
|
case SpvOpTypeStruct: {
|
|
// SPIR-V requires the index to be an OpConstant.
|
|
// We need to know the index literal value so we can compute the next
|
|
// pointee type.
|
|
if (index_inst->opcode() != SpvOpConstant ||
|
|
!constant_mgr->GetConstantFromInst(index_inst)
|
|
->type()
|
|
->AsInteger()) {
|
|
Fail() << "Member index into struct is not a constant integer: "
|
|
<< index_inst->PrettyPrint(
|
|
SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)
|
|
<< "\nin access chain: "
|
|
<< inst.PrettyPrint(SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
|
|
return;
|
|
}
|
|
const auto num_members = pointee_type->NumInOperands();
|
|
const auto* index_constant =
|
|
constant_mgr->GetConstantFromInst(index_inst);
|
|
// Get the sign-extended value, since access index is always treated as
|
|
// signed.
|
|
const auto index_value = index_constant->GetSignExtendedValue();
|
|
if (index_value < 0 || index_value >= num_members) {
|
|
Fail() << "Member index " << index_value
|
|
<< " is out of bounds for struct type: "
|
|
<< pointee_type->PrettyPrint(
|
|
SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)
|
|
<< "\nin access chain: "
|
|
<< inst.PrettyPrint(SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
|
|
return;
|
|
}
|
|
pointee_type = GetDef(pointee_type->GetSingleWordInOperand(
|
|
static_cast<uint32_t>(index_value)));
|
|
// No need to clamp this index. We just checked that it's valid.
|
|
} break;
|
|
|
|
case SpvOpTypeRuntimeArray: {
|
|
auto* array_len = MakeRuntimeArrayLengthInst(&inst, idx);
|
|
if (!array_len) { // We've already signaled an error.
|
|
return;
|
|
}
|
|
clamp_to_count(idx, array_len);
|
|
pointee_type = GetDef(pointee_type->GetSingleWordOperand(1));
|
|
} break;
|
|
|
|
default:
|
|
Fail() << " Unhandled pointee type for access chain "
|
|
<< pointee_type->PrettyPrint(
|
|
SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
|
|
}
|
|
}
|
|
}
|
|
|
|
uint32_t GraphicsRobustAccessPass::GetGlslInsts() {
|
|
if (module_status_.glsl_insts_id == 0) {
|
|
// This string serves double-duty as raw data for a string and for a vector
|
|
// of 32-bit words
|
|
const char glsl[] = "GLSL.std.450\0\0\0\0";
|
|
const size_t glsl_str_byte_len = 16;
|
|
// Use an existing import if we can.
|
|
for (auto& inst : context()->module()->ext_inst_imports()) {
|
|
const auto& name_words = inst.GetInOperand(0).words;
|
|
if (0 == std::strncmp(reinterpret_cast<const char*>(name_words.data()),
|
|
glsl, glsl_str_byte_len)) {
|
|
module_status_.glsl_insts_id = inst.result_id();
|
|
}
|
|
}
|
|
if (module_status_.glsl_insts_id == 0) {
|
|
// Make a new import instruction.
|
|
module_status_.glsl_insts_id = TakeNextId();
|
|
std::vector<uint32_t> words(glsl_str_byte_len / sizeof(uint32_t));
|
|
std::memcpy(words.data(), glsl, glsl_str_byte_len);
|
|
auto import_inst = MakeUnique<Instruction>(
|
|
context(), SpvOpExtInstImport, 0, module_status_.glsl_insts_id,
|
|
std::initializer_list<Operand>{
|
|
Operand{SPV_OPERAND_TYPE_LITERAL_STRING, std::move(words)}});
|
|
Instruction* inst = import_inst.get();
|
|
context()->module()->AddExtInstImport(std::move(import_inst));
|
|
module_status_.modified = true;
|
|
context()->AnalyzeDefUse(inst);
|
|
// Reanalyze the feature list, since we added an extended instruction
|
|
// set improt.
|
|
context()->get_feature_mgr()->Analyze(context()->module());
|
|
}
|
|
}
|
|
return module_status_.glsl_insts_id;
|
|
}
|
|
|
|
opt::Instruction* opt::GraphicsRobustAccessPass::GetValueForType(
|
|
uint64_t value, const analysis::Integer* type) {
|
|
auto* mgr = context()->get_constant_mgr();
|
|
assert(type->width() <= 64);
|
|
std::vector<uint32_t> words;
|
|
words.push_back(uint32_t(value));
|
|
if (type->width() > 32) {
|
|
words.push_back(uint32_t(value >> 32u));
|
|
}
|
|
const auto* constant = mgr->GetConstant(type, words);
|
|
return mgr->GetDefiningInstruction(
|
|
constant, context()->get_type_mgr()->GetTypeInstruction(type));
|
|
}
|
|
|
|
opt::Instruction* opt::GraphicsRobustAccessPass::WidenInteger(
|
|
bool sign_extend, uint32_t bit_width, Instruction* value,
|
|
Instruction* before_inst) {
|
|
analysis::Integer unsigned_type_for_query(bit_width, false);
|
|
auto* type_mgr = context()->get_type_mgr();
|
|
auto* unsigned_type = type_mgr->GetRegisteredType(&unsigned_type_for_query);
|
|
auto type_id = context()->get_type_mgr()->GetId(unsigned_type);
|
|
auto conversion_id = TakeNextId();
|
|
auto* conversion = InsertInst(
|
|
before_inst, (sign_extend ? SpvOpSConvert : SpvOpUConvert), type_id,
|
|
conversion_id, {{SPV_OPERAND_TYPE_ID, {value->result_id()}}});
|
|
return conversion;
|
|
}
|
|
|
|
Instruction* GraphicsRobustAccessPass::MakeClampInst(Instruction* x,
|
|
Instruction* min,
|
|
Instruction* max,
|
|
Instruction* where) {
|
|
// Get IDs of instructions we'll be referencing. Evaluate them before calling
|
|
// the function so we force a deterministic ordering in case both of them need
|
|
// to take a new ID.
|
|
const uint32_t glsl_insts_id = GetGlslInsts();
|
|
uint32_t clamp_id = TakeNextId();
|
|
assert(x->type_id() == min->type_id());
|
|
assert(x->type_id() == max->type_id());
|
|
auto* clamp_inst = InsertInst(
|
|
where, SpvOpExtInst, x->type_id(), clamp_id,
|
|
{
|
|
{SPV_OPERAND_TYPE_ID, {glsl_insts_id}},
|
|
{SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER, {GLSLstd450UClamp}},
|
|
{SPV_OPERAND_TYPE_ID, {x->result_id()}},
|
|
{SPV_OPERAND_TYPE_ID, {min->result_id()}},
|
|
{SPV_OPERAND_TYPE_ID, {max->result_id()}},
|
|
});
|
|
return clamp_inst;
|
|
}
|
|
|
|
Instruction* GraphicsRobustAccessPass::MakeRuntimeArrayLengthInst(
|
|
Instruction* access_chain, uint32_t operand_index) {
|
|
// The Index parameter to the access chain at |operand_index| is indexing
|
|
// *into* the runtime-array. To get the number of elements in the runtime
|
|
// array we need a pointer to the Block-decorated struct that contains the
|
|
// runtime array. So conceptually we have to go 2 steps backward in the
|
|
// access chain. The two steps backward might forces us to traverse backward
|
|
// across multiple dominating instructions.
|
|
auto* type_mgr = context()->get_type_mgr();
|
|
|
|
// How many access chain indices do we have to unwind to find the pointer
|
|
// to the struct containing the runtime array?
|
|
uint32_t steps_remaining = 2;
|
|
// Find or create an instruction computing the pointer to the structure
|
|
// containing the runtime array.
|
|
// Walk backward through pointer address calculations until we either get
|
|
// to exactly the right base pointer, or to an access chain instruction
|
|
// that we can replicate but truncate to compute the address of the right
|
|
// struct.
|
|
Instruction* current_access_chain = access_chain;
|
|
Instruction* pointer_to_containing_struct = nullptr;
|
|
while (steps_remaining > 0) {
|
|
switch (current_access_chain->opcode()) {
|
|
case SpvOpCopyObject:
|
|
// Whoops. Walk right through this one.
|
|
current_access_chain =
|
|
GetDef(current_access_chain->GetSingleWordInOperand(0));
|
|
break;
|
|
case SpvOpAccessChain:
|
|
case SpvOpInBoundsAccessChain: {
|
|
const int first_index_operand = 3;
|
|
// How many indices in this access chain contribute to getting us
|
|
// to an element in the runtime array?
|
|
const auto num_contributing_indices =
|
|
current_access_chain == access_chain
|
|
? operand_index - (first_index_operand - 1)
|
|
: current_access_chain->NumInOperands() - 1 /* skip the base */;
|
|
Instruction* base =
|
|
GetDef(current_access_chain->GetSingleWordInOperand(0));
|
|
if (num_contributing_indices == steps_remaining) {
|
|
// The base pointer points to the structure.
|
|
pointer_to_containing_struct = base;
|
|
steps_remaining = 0;
|
|
break;
|
|
} else if (num_contributing_indices < steps_remaining) {
|
|
// Peel off the index and keep going backward.
|
|
steps_remaining -= num_contributing_indices;
|
|
current_access_chain = base;
|
|
} else {
|
|
// This access chain has more indices than needed. Generate a new
|
|
// access chain instruction, but truncating the list of indices.
|
|
const int base_operand = 2;
|
|
// We'll use the base pointer and the indices up to but not including
|
|
// the one indexing into the runtime array.
|
|
Instruction::OperandList ops;
|
|
// Use the base pointer
|
|
ops.push_back(current_access_chain->GetOperand(base_operand));
|
|
const uint32_t num_indices_to_keep =
|
|
num_contributing_indices - steps_remaining - 1;
|
|
for (uint32_t i = 0; i <= num_indices_to_keep; i++) {
|
|
ops.push_back(
|
|
current_access_chain->GetOperand(first_index_operand + i));
|
|
}
|
|
// Compute the type of the result of the new access chain. Start at
|
|
// the base and walk the indices in a forward direction.
|
|
auto* constant_mgr = context()->get_constant_mgr();
|
|
std::vector<uint32_t> indices_for_type;
|
|
for (uint32_t i = 0; i < ops.size() - 1; i++) {
|
|
uint32_t index_for_type_calculation = 0;
|
|
Instruction* index =
|
|
GetDef(current_access_chain->GetSingleWordOperand(
|
|
first_index_operand + i));
|
|
if (auto* index_constant =
|
|
constant_mgr->GetConstantFromInst(index)) {
|
|
// We only need 32 bits. For the type calculation, it's sufficient
|
|
// to take the zero-extended value. It only matters for the struct
|
|
// case, and struct member indices are unsigned.
|
|
index_for_type_calculation =
|
|
uint32_t(index_constant->GetZeroExtendedValue());
|
|
} else {
|
|
// Indexing into a variably-sized thing like an array. Use 0.
|
|
index_for_type_calculation = 0;
|
|
}
|
|
indices_for_type.push_back(index_for_type_calculation);
|
|
}
|
|
auto* base_ptr_type = type_mgr->GetType(base->type_id())->AsPointer();
|
|
auto* base_pointee_type = base_ptr_type->pointee_type();
|
|
auto* new_access_chain_result_pointee_type =
|
|
type_mgr->GetMemberType(base_pointee_type, indices_for_type);
|
|
const uint32_t new_access_chain_type_id = type_mgr->FindPointerToType(
|
|
type_mgr->GetId(new_access_chain_result_pointee_type),
|
|
base_ptr_type->storage_class());
|
|
|
|
// Create the instruction and insert it.
|
|
const auto new_access_chain_id = TakeNextId();
|
|
auto* new_access_chain =
|
|
InsertInst(current_access_chain, current_access_chain->opcode(),
|
|
new_access_chain_type_id, new_access_chain_id, ops);
|
|
pointer_to_containing_struct = new_access_chain;
|
|
steps_remaining = 0;
|
|
break;
|
|
}
|
|
} break;
|
|
default:
|
|
Fail() << "Unhandled access chain in logical addressing mode passes "
|
|
"through "
|
|
<< current_access_chain->PrettyPrint(
|
|
SPV_BINARY_TO_TEXT_OPTION_SHOW_BYTE_OFFSET |
|
|
SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
|
|
return nullptr;
|
|
}
|
|
}
|
|
assert(pointer_to_containing_struct);
|
|
auto* pointee_type =
|
|
type_mgr->GetType(pointer_to_containing_struct->type_id())
|
|
->AsPointer()
|
|
->pointee_type();
|
|
|
|
auto* struct_type = pointee_type->AsStruct();
|
|
const uint32_t member_index_of_runtime_array =
|
|
uint32_t(struct_type->element_types().size() - 1);
|
|
// Create the length-of-array instruction before the original access chain,
|
|
// but after the generation of the pointer to the struct.
|
|
const auto array_len_id = TakeNextId();
|
|
analysis::Integer uint_type_for_query(32, false);
|
|
auto* uint_type = type_mgr->GetRegisteredType(&uint_type_for_query);
|
|
auto* array_len = InsertInst(
|
|
access_chain, SpvOpArrayLength, type_mgr->GetId(uint_type), array_len_id,
|
|
{{SPV_OPERAND_TYPE_ID, {pointer_to_containing_struct->result_id()}},
|
|
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {member_index_of_runtime_array}}});
|
|
return array_len;
|
|
}
|
|
|
|
spv_result_t GraphicsRobustAccessPass::ClampCoordinateForImageTexelPointer(
|
|
opt::Instruction* image_texel_pointer) {
|
|
// TODO(dneto): Write tests for this code.
|
|
return SPV_SUCCESS;
|
|
|
|
// Example:
|
|
// %texel_ptr = OpImageTexelPointer %texel_ptr_type %image_ptr %coord
|
|
// %sample
|
|
//
|
|
// We want to clamp %coord components between vector-0 and the result
|
|
// of OpImageQuerySize acting on the underlying image. So insert:
|
|
// %image = OpLoad %image_type %image_ptr
|
|
// %query_size = OpImageQuerySize %query_size_type %image
|
|
//
|
|
// For a multi-sampled image, %sample is the sample index, and we need
|
|
// to clamp it between zero and the number of samples in the image.
|
|
// %sample_count = OpImageQuerySamples %uint %image
|
|
// %max_sample_index = OpISub %uint %sample_count %uint_1
|
|
// For non-multi-sampled images, the sample index must be constant zero.
|
|
|
|
auto* def_use_mgr = context()->get_def_use_mgr();
|
|
auto* type_mgr = context()->get_type_mgr();
|
|
auto* constant_mgr = context()->get_constant_mgr();
|
|
|
|
auto* image_ptr = GetDef(image_texel_pointer->GetSingleWordInOperand(0));
|
|
auto* image_ptr_type = GetDef(image_ptr->type_id());
|
|
auto image_type_id = image_ptr_type->GetSingleWordInOperand(1);
|
|
auto* image_type = GetDef(image_type_id);
|
|
auto* coord = GetDef(image_texel_pointer->GetSingleWordInOperand(1));
|
|
auto* samples = GetDef(image_texel_pointer->GetSingleWordInOperand(2));
|
|
|
|
// We will modify the module, at least by adding image query instructions.
|
|
module_status_.modified = true;
|
|
|
|
// Declare the ImageQuery capability if the module doesn't already have it.
|
|
auto* feature_mgr = context()->get_feature_mgr();
|
|
if (!feature_mgr->HasCapability(SpvCapabilityImageQuery)) {
|
|
auto cap = MakeUnique<Instruction>(
|
|
context(), SpvOpCapability, 0, 0,
|
|
std::initializer_list<Operand>{
|
|
{SPV_OPERAND_TYPE_CAPABILITY, {SpvCapabilityImageQuery}}});
|
|
def_use_mgr->AnalyzeInstDefUse(cap.get());
|
|
context()->AddCapability(std::move(cap));
|
|
feature_mgr->Analyze(context()->module());
|
|
}
|
|
|
|
// OpImageTexelPointer is used to translate a coordinate and sample index
|
|
// into an address for use with an atomic operation. That is, it may only
|
|
// used with what Vulkan calls a "storage image"
|
|
// (OpTypeImage parameter Sampled=2).
|
|
// Note: A storage image never has a level-of-detail associated with it.
|
|
|
|
// Constraints on the sample id:
|
|
// - Only 2D images can be multi-sampled: OpTypeImage parameter MS=1
|
|
// only if Dim=2D.
|
|
// - Non-multi-sampled images (OpTypeImage parameter MS=0) must use
|
|
// sample ID to a constant 0.
|
|
|
|
// The coordinate is treated as unsigned, and should be clamped against the
|
|
// image "size", returned by OpImageQuerySize. (Note: OpImageQuerySizeLod
|
|
// is only usable with a sampled image, i.e. its image type has Sampled=1).
|
|
|
|
// Determine the result type for the OpImageQuerySize.
|
|
// For non-arrayed images:
|
|
// non-Cube:
|
|
// - Always the same as the coordinate type
|
|
// Cube:
|
|
// - Use all but the last component of the coordinate (which is the face
|
|
// index from 0 to 5).
|
|
// For arrayed images (in Vulkan the Dim is 1D, 2D, or Cube):
|
|
// non-Cube:
|
|
// - A vector with the components in the coordinate, and one more for
|
|
// the layer index.
|
|
// Cube:
|
|
// - The same as the coordinate type: 3-element integer vector.
|
|
// - The third component from the size query is the layer count.
|
|
// - The third component in the texel pointer calculation is
|
|
// 6 * layer + face, where 0 <= face < 6.
|
|
// Cube: Use all but the last component of the coordinate (which is the face
|
|
// index from 0 to 5).
|
|
const auto dim = SpvDim(image_type->GetSingleWordInOperand(1));
|
|
const bool arrayed = image_type->GetSingleWordInOperand(3) == 1;
|
|
const bool multisampled = image_type->GetSingleWordInOperand(4) != 0;
|
|
const auto query_num_components = [dim, arrayed, this]() -> int {
|
|
const int arrayness_bonus = arrayed ? 1 : 0;
|
|
int num_coords = 0;
|
|
switch (dim) {
|
|
case SpvDimBuffer:
|
|
case SpvDim1D:
|
|
num_coords = 1;
|
|
break;
|
|
case SpvDimCube:
|
|
// For cube, we need bounds for x, y, but not face.
|
|
case SpvDimRect:
|
|
case SpvDim2D:
|
|
num_coords = 2;
|
|
break;
|
|
case SpvDim3D:
|
|
num_coords = 3;
|
|
break;
|
|
case SpvDimSubpassData:
|
|
case SpvDimMax:
|
|
return Fail() << "Invalid image dimension for OpImageTexelPointer: "
|
|
<< int(dim);
|
|
break;
|
|
}
|
|
return num_coords + arrayness_bonus;
|
|
}();
|
|
const auto* coord_component_type = [type_mgr, coord]() {
|
|
const analysis::Type* coord_type = type_mgr->GetType(coord->type_id());
|
|
if (auto* vector_type = coord_type->AsVector()) {
|
|
return vector_type->element_type()->AsInteger();
|
|
}
|
|
return coord_type->AsInteger();
|
|
}();
|
|
// For now, only handle 32-bit case for coordinates.
|
|
if (!coord_component_type) {
|
|
return Fail() << " Coordinates for OpImageTexelPointer are not integral: "
|
|
<< image_texel_pointer->PrettyPrint(
|
|
SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
|
|
}
|
|
if (coord_component_type->width() != 32) {
|
|
return Fail() << " Expected OpImageTexelPointer coordinate components to "
|
|
"be 32-bits wide. They are "
|
|
<< coord_component_type->width() << " bits. "
|
|
<< image_texel_pointer->PrettyPrint(
|
|
SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
|
|
}
|
|
const auto* query_size_type =
|
|
[type_mgr, coord_component_type,
|
|
query_num_components]() -> const analysis::Type* {
|
|
if (query_num_components == 1) return coord_component_type;
|
|
analysis::Vector proposed(coord_component_type, query_num_components);
|
|
return type_mgr->GetRegisteredType(&proposed);
|
|
}();
|
|
|
|
const uint32_t image_id = TakeNextId();
|
|
auto* image =
|
|
InsertInst(image_texel_pointer, SpvOpLoad, image_type_id, image_id,
|
|
{{SPV_OPERAND_TYPE_ID, {image_ptr->result_id()}}});
|
|
|
|
const uint32_t query_size_id = TakeNextId();
|
|
auto* query_size =
|
|
InsertInst(image_texel_pointer, SpvOpImageQuerySize,
|
|
type_mgr->GetTypeInstruction(query_size_type), query_size_id,
|
|
{{SPV_OPERAND_TYPE_ID, {image->result_id()}}});
|
|
|
|
auto* component_1 = constant_mgr->GetConstant(coord_component_type, {1});
|
|
const uint32_t component_1_id =
|
|
constant_mgr->GetDefiningInstruction(component_1)->result_id();
|
|
auto* component_0 = constant_mgr->GetConstant(coord_component_type, {0});
|
|
const uint32_t component_0_id =
|
|
constant_mgr->GetDefiningInstruction(component_0)->result_id();
|
|
|
|
// If the image is a cube array, then the last component of the queried
|
|
// size is the layer count. In the query, we have to accomodate folding
|
|
// in the face index ranging from 0 through 5. The inclusive upper bound
|
|
// on the third coordinate therefore is multiplied by 6.
|
|
auto* query_size_including_faces = query_size;
|
|
if (arrayed && (dim == SpvDimCube)) {
|
|
// Multiply the last coordinate by 6.
|
|
auto* component_6 = constant_mgr->GetConstant(coord_component_type, {6});
|
|
const uint32_t component_6_id =
|
|
constant_mgr->GetDefiningInstruction(component_6)->result_id();
|
|
assert(query_num_components == 3);
|
|
auto* multiplicand = constant_mgr->GetConstant(
|
|
query_size_type, {component_1_id, component_1_id, component_6_id});
|
|
auto* multiplicand_inst =
|
|
constant_mgr->GetDefiningInstruction(multiplicand);
|
|
const auto query_size_including_faces_id = TakeNextId();
|
|
query_size_including_faces = InsertInst(
|
|
image_texel_pointer, SpvOpIMul,
|
|
type_mgr->GetTypeInstruction(query_size_type),
|
|
query_size_including_faces_id,
|
|
{{SPV_OPERAND_TYPE_ID, {query_size_including_faces->result_id()}},
|
|
{SPV_OPERAND_TYPE_ID, {multiplicand_inst->result_id()}}});
|
|
}
|
|
|
|
// Make a coordinate-type with all 1 components.
|
|
auto* coordinate_1 =
|
|
query_num_components == 1
|
|
? component_1
|
|
: constant_mgr->GetConstant(
|
|
query_size_type,
|
|
std::vector<uint32_t>(query_num_components, component_1_id));
|
|
// Make a coordinate-type with all 1 components.
|
|
auto* coordinate_0 =
|
|
query_num_components == 0
|
|
? component_0
|
|
: constant_mgr->GetConstant(
|
|
query_size_type,
|
|
std::vector<uint32_t>(query_num_components, component_0_id));
|
|
|
|
const uint32_t query_max_including_faces_id = TakeNextId();
|
|
auto* query_max_including_faces = InsertInst(
|
|
image_texel_pointer, SpvOpISub,
|
|
type_mgr->GetTypeInstruction(query_size_type),
|
|
query_max_including_faces_id,
|
|
{{SPV_OPERAND_TYPE_ID, {query_size_including_faces->result_id()}},
|
|
{SPV_OPERAND_TYPE_ID,
|
|
{constant_mgr->GetDefiningInstruction(coordinate_1)->result_id()}}});
|
|
|
|
// Clamp the coordinate
|
|
auto* clamp_coord =
|
|
MakeClampInst(coord, constant_mgr->GetDefiningInstruction(coordinate_0),
|
|
query_max_including_faces, image_texel_pointer);
|
|
image_texel_pointer->SetInOperand(1, {clamp_coord->result_id()});
|
|
|
|
// Clamp the sample index
|
|
if (multisampled) {
|
|
// Get the sample count via OpImageQuerySamples
|
|
const auto query_samples_id = TakeNextId();
|
|
auto* query_samples = InsertInst(
|
|
image_texel_pointer, SpvOpImageQuerySamples,
|
|
constant_mgr->GetDefiningInstruction(component_0)->type_id(),
|
|
query_samples_id, {{SPV_OPERAND_TYPE_ID, {image->result_id()}}});
|
|
|
|
const auto max_samples_id = TakeNextId();
|
|
auto* max_samples = InsertInst(image_texel_pointer, SpvOpImageQuerySamples,
|
|
query_samples->type_id(), max_samples_id,
|
|
{{SPV_OPERAND_TYPE_ID, {query_samples_id}},
|
|
{SPV_OPERAND_TYPE_ID, {component_1_id}}});
|
|
|
|
auto* clamp_samples = MakeClampInst(
|
|
samples, constant_mgr->GetDefiningInstruction(coordinate_0),
|
|
max_samples, image_texel_pointer);
|
|
image_texel_pointer->SetInOperand(2, {clamp_samples->result_id()});
|
|
|
|
} else {
|
|
// Just replace it with 0. Don't even check what was there before.
|
|
image_texel_pointer->SetInOperand(2, {component_0_id});
|
|
}
|
|
|
|
def_use_mgr->AnalyzeInstUse(image_texel_pointer);
|
|
|
|
return SPV_SUCCESS;
|
|
}
|
|
|
|
opt::Instruction* GraphicsRobustAccessPass::InsertInst(
|
|
opt::Instruction* where_inst, SpvOp opcode, uint32_t type_id,
|
|
uint32_t result_id, const Instruction::OperandList& operands) {
|
|
module_status_.modified = true;
|
|
auto* result = where_inst->InsertBefore(
|
|
MakeUnique<Instruction>(context(), opcode, type_id, result_id, operands));
|
|
context()->get_def_use_mgr()->AnalyzeInstDefUse(result);
|
|
auto* basic_block = context()->get_instr_block(where_inst);
|
|
context()->set_instr_block(result, basic_block);
|
|
return result;
|
|
}
|
|
|
|
} // namespace opt
|
|
} // namespace spvtools
|