blob: 57424e5745fa981fd73019d1affd1c3830408427 [file] [edit]
// Copyright (c) 2026 Google 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 <algorithm>
#include <cassert>
#include <ostream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "source/spirv_constant.h"
#include "source/spirv_target_env.h"
#include "source/util/hash_combine.h"
#include "source/val/validation_state.h"
namespace spvtools {
namespace val {
namespace {
enum class LayoutMode : uint8_t {
// Vulkan scalar block rules
kScalar,
// Vulkan standard alignment rules (i.e. std430)
kStandard,
// Vulkan extended alignment rules (i.e. std140)
kExtended,
};
std::ostream& operator<<(std::ostream& str, const LayoutMode& mode) {
switch (mode) {
case LayoutMode::kScalar:
str << "scalar";
break;
case LayoutMode::kStandard:
str << "standard";
break;
case LayoutMode::kExtended:
str << "extended";
break;
}
return str;
}
enum class LayoutRequirement : uint8_t {
// Must be laid out
kRequired,
// Must not be laid out
kProhibited,
// Either laid or not
kAllowed,
};
struct Impl {
ValidationState_t& vstate;
// Relevant information to describe a memory instruction for the purposes of
// layout validation.
struct MemoryReference {
// The data type of the memory instruction (after stripping any descriptor
// array).
uint32_t type_id = 0;
// The descriptor array type id (if there is one).
uint32_t descriptor_array_id = 0;
// The storage class for the memory instruction.
spv::StorageClass storage_class;
// The layout mode (only relevant if a layout is required).
LayoutMode layout;
// The layout requirement for the instruction.
LayoutRequirement requirement;
// Whether it is an untyped pointer base.
bool untyped = false;
};
// Matrix constraints from a struct member to carry to the actual matrix type.
struct MatrixConstraints {
uint32_t stride = 0;
bool col_major = true;
};
// Cache valid checks for types that should have no layout.
std::unordered_set<uint32_t> no_layout_cache_;
// Struct member info
struct MemberInfo {
// Structure member index (note: index + 1 is instruction index).
uint32_t index;
// Whether or not an Offset/OffsetIdEXT decoration is present.
bool has_offset = false;
// Offset value. Max uint32_t is used for no evaluation (e.g. spec
// constant).
uint32_t offset = std::numeric_limits<uint32_t>::max();
// Whether or not RowMajor or ColMajor decoration is present.
bool has_matrix = false;
// Matrix constraints (stride and majorness).
MatrixConstraints matrix_constraints;
};
// Cache of structure member information.
std::unordered_map<uint32_t, std::vector<MemberInfo>> struct_members_;
struct LayoutKey {
uint32_t type_id = 0;
LayoutMode layout;
uint32_t incoming_offset = 0;
MatrixConstraints matrix_constraints{};
bool operator==(const LayoutKey& other) const {
return type_id == other.type_id && layout == other.layout &&
incoming_offset == other.incoming_offset &&
matrix_constraints.stride == other.matrix_constraints.stride &&
matrix_constraints.col_major == other.matrix_constraints.col_major;
}
};
struct LayoutKeyHash {
size_t operator()(const LayoutKey& key) const noexcept {
return spvtools::utils::hash_combine(
0, key.type_id, static_cast<uint32_t>(key.layout),
key.incoming_offset, key.matrix_constraints.stride,
key.matrix_constraints.col_major);
}
};
// Caches valid results of CheckLayout calls.
std::unordered_set<LayoutKey, LayoutKeyHash> layout_cache_;
// Returns the layout requirements for `sc`.
// Workgroup is expected to be explicitly laid out if `is_block` is true.
// UniformConstant is expected to be explicitly laid out if `descriptor_heap`
// is true.
LayoutRequirement GetStorageClassRequirement(spv::StorageClass sc,
bool is_block,
bool descriptor_heap) {
switch (sc) {
case spv::StorageClass::Workgroup:
return is_block ? LayoutRequirement::kRequired
: LayoutRequirement::kProhibited;
case spv::StorageClass::StorageBuffer:
case spv::StorageClass::Uniform:
case spv::StorageClass::PushConstant:
case spv::StorageClass::PhysicalStorageBuffer:
return LayoutRequirement::kRequired;
case spv::StorageClass::UniformConstant:
return descriptor_heap ? LayoutRequirement::kRequired
: LayoutRequirement::kProhibited;
case spv::StorageClass::Function:
case spv::StorageClass::Private:
return vstate.version() <= SPV_SPIRV_VERSION_WORD(1, 4)
? LayoutRequirement::kAllowed
: LayoutRequirement::kProhibited;
case spv::StorageClass::Input:
case spv::StorageClass::Output:
// Block is used generally and mesh shaders use Offset.
// TODO: This is a little over permissive.
return LayoutRequirement::kAllowed;
default:
// TODO: Some storage classes in ray tracing use explicit layout
// decorations, but it is not well documented which. For now treat
// other storage classes as allowed to be laid out. See Vulkan
// internal issue 4192.
return LayoutRequirement::kAllowed;
}
}
// Returns the layout rules for `sc`.
LayoutMode GetStorageClassLayout(spv::StorageClass sc, bool is_buffer_block) {
switch (sc) {
case spv::StorageClass::Workgroup:
return vstate.options()->workgroup_scalar_block_layout
? LayoutMode::kScalar
: LayoutMode::kStandard;
break;
case spv::StorageClass::StorageBuffer:
case spv::StorageClass::PushConstant:
case spv::StorageClass::UniformConstant:
case spv::StorageClass::PhysicalStorageBuffer:
return vstate.options()->scalar_block_layout ? LayoutMode::kScalar
: LayoutMode::kStandard;
break;
case spv::StorageClass::Uniform:
return vstate.options()->scalar_block_layout
? LayoutMode::kScalar
: ((is_buffer_block ||
vstate.options()->uniform_buffer_standard_layout)
? LayoutMode::kStandard
: LayoutMode::kExtended);
break;
default:
break;
}
return LayoutMode::kStandard;
}
// Returns true if `inst` is a memory reference instruction
// Populates `reference` with the necessary information.
//
// The following are interesting memory references:
// For typed pointers:
// * OpVariable
// * Memory instructions on PhysicalStorageBuffer
// * OpBufferPointerEXT
// For untyped pointers:
// * All memory instructions
bool GetMemoryReference(const Instruction* inst, MemoryReference* reference) {
auto* type_inst = vstate.FindDef(inst->type_id());
if (inst->opcode() == spv::Op::OpVariable) {
auto sc = type_inst->GetOperandAs<spv::StorageClass>(1u);
const bool is_descriptor_heap = vstate.IsDescriptorHeapBaseVariable(inst);
reference->storage_class = sc;
reference->type_id = type_inst->GetOperandAs<uint32_t>(2u);
const bool is_block_decorated =
vstate.GetIdOpcode(reference->type_id) == spv::Op::OpTypeStruct &&
vstate.HasDecoration(reference->type_id, spv::Decoration::Block);
reference->requirement = GetStorageClassRequirement(
sc, is_block_decorated, is_descriptor_heap);
// Unwrap the descriptor array.
if (sc == spv::StorageClass::StorageBuffer ||
sc == spv::StorageClass::Uniform ||
sc == spv::StorageClass::UniformConstant) {
const auto* data_type = vstate.FindDef(reference->type_id);
if (data_type->opcode() == spv::Op::OpTypeArray ||
data_type->opcode() == spv::Op::OpTypeRuntimeArray) {
reference->descriptor_array_id = reference->type_id;
reference->type_id = data_type->GetOperandAs<uint32_t>(1u);
}
}
const bool buffer_block =
vstate.GetIdOpcode(reference->type_id) == spv::Op::OpTypeStruct &&
vstate.HasDecoration(reference->type_id,
spv::Decoration::BufferBlock);
reference->layout = GetStorageClassLayout(sc, buffer_block);
return true;
} else if (type_inst && type_inst->opcode() == spv::Op::OpTypePointer &&
type_inst->GetOperandAs<spv::StorageClass>(1u) ==
spv::StorageClass::PhysicalStorageBuffer) {
reference->storage_class = spv::StorageClass::PhysicalStorageBuffer;
reference->type_id = type_inst->GetOperandAs<uint32_t>(2u);
reference->layout = GetStorageClassLayout(
spv::StorageClass::PhysicalStorageBuffer, false);
reference->requirement = LayoutRequirement::kRequired;
return true;
} else if (inst->opcode() == spv::Op::OpBufferPointerEXT &&
type_inst->opcode() == spv::Op::OpTypePointer) {
auto sc = type_inst->GetOperandAs<spv::StorageClass>(1u);
reference->storage_class = sc;
uint32_t pointee_ty_id = type_inst->GetOperandAs<uint32_t>(2u);
const bool buffer_block =
vstate.GetIdOpcode(pointee_ty_id) == spv::Op::OpTypeStruct &&
vstate.HasDecoration(pointee_ty_id, spv::Decoration::BufferBlock);
reference->layout = GetStorageClassLayout(sc, buffer_block);
reference->requirement = LayoutRequirement::kRequired;
reference->type_id = pointee_ty_id;
return true;
} else if (vstate.HasCapability(spv::Capability::UntypedPointersKHR) &&
spvIsVulkanEnv(vstate.context()->target_env)) {
uint32_t ptr_ty_id = 0;
uint32_t data_ty_id = 0;
switch (inst->opcode()) {
case spv::Op::OpUntypedVariableKHR:
if (inst->operands().size() > 3) {
ptr_ty_id = inst->type_id();
data_ty_id = inst->GetOperandAs<uint32_t>(3u);
} else {
return false;
}
break;
case spv::Op::OpUntypedAccessChainKHR:
case spv::Op::OpUntypedInBoundsAccessChainKHR:
case spv::Op::OpUntypedPtrAccessChainKHR:
case spv::Op::OpUntypedInBoundsPtrAccessChainKHR:
ptr_ty_id = inst->type_id();
data_ty_id = inst->GetOperandAs<uint32_t>(2);
break;
case spv::Op::OpLoad:
if (vstate.GetIdOpcode(vstate.GetOperandTypeId(inst, 2)) ==
spv::Op::OpTypeUntypedPointerKHR) {
const auto ptr_id = inst->GetOperandAs<uint32_t>(2);
ptr_ty_id = vstate.FindDef(ptr_id)->type_id();
data_ty_id = inst->type_id();
} else {
return false;
}
break;
case spv::Op::OpStore:
if (vstate.GetIdOpcode(vstate.GetOperandTypeId(inst, 0)) ==
spv::Op::OpTypeUntypedPointerKHR) {
const auto ptr_id = inst->GetOperandAs<uint32_t>(0);
ptr_ty_id = vstate.FindDef(ptr_id)->type_id();
data_ty_id = vstate.GetOperandTypeId(inst, 1);
} else {
return false;
}
break;
case spv::Op::OpUntypedArrayLengthKHR:
ptr_ty_id =
vstate.FindDef(inst->GetOperandAs<uint32_t>(3))->type_id();
data_ty_id = inst->GetOperandAs<uint32_t>(2);
break;
default:
return false;
}
// If the data type is an array that contains a Block- or
// BufferBlock-decorated struct, then use the struct for layout checks
// instead of the array. In this case, the array represents a descriptor
// array which should not have an explicit layout.
const auto* data_type = vstate.FindDef(data_ty_id);
if (data_type->opcode() == spv::Op::OpTypeArray ||
data_type->opcode() == spv::Op::OpTypeRuntimeArray) {
uint32_t ele_ty_id = data_type->GetOperandAs<uint32_t>(1u);
if (vstate.HasDecoration(ele_ty_id, spv::Decoration::Block) ||
vstate.HasDecoration(ele_ty_id, spv::Decoration::BufferBlock)) {
reference->descriptor_array_id = data_ty_id;
data_ty_id = ele_ty_id;
}
}
auto sc = vstate.FindDef(ptr_ty_id)->GetOperandAs<spv::StorageClass>(1u);
reference->storage_class = sc;
reference->type_id = data_ty_id;
reference->requirement = LayoutRequirement::kRequired;
reference->untyped = true;
const bool buffer_block =
vstate.GetIdOpcode(data_ty_id) == spv::Op::OpTypeStruct &&
vstate.HasDecoration(data_ty_id, spv::Decoration::BufferBlock);
reference->layout = GetStorageClassLayout(sc, buffer_block);
return true;
}
// Not a memory reference.
return false;
}
// Checks that no instruction in the type tree of `type_id` has any explicit
// layout decorations.
spv_result_t CheckNoLayout(const Instruction* inst, uint32_t type_id,
spv::StorageClass sc) {
if (no_layout_cache_.count(type_id)) {
return SPV_SUCCESS;
}
const auto* type_inst = vstate.FindDef(type_id);
if (type_inst->opcode() == spv::Op::OpTypePointer) {
// PhysicalStorageBuffer and variable pointers can have ArrayStride
// decorations even if they are stored in a non-laid storage class (e.g.
// Function).
auto ptr_sc = type_inst->GetOperandAs<spv::StorageClass>(1u);
if (GetStorageClassRequirement(ptr_sc, true, false) !=
LayoutRequirement::kProhibited) {
return SPV_SUCCESS;
}
}
const auto& id_decs = vstate.id_decorations();
const auto iter = id_decs.find(type_id);
if (iter != id_decs.end()) {
for (const auto& d : iter->second) {
const spv::Decoration dec = d.dec_type();
if (dec == spv::Decoration::Block ||
dec == spv::Decoration::BufferBlock ||
dec == spv::Decoration::Offset ||
dec == spv::Decoration::OffsetIdEXT ||
dec == spv::Decoration::ArrayStride ||
dec == spv::Decoration::ArrayStrideIdEXT ||
dec == spv::Decoration::MatrixStride ||
dec == spv::Decoration::RowMajor ||
dec == spv::Decoration::ColMajor) {
return vstate.diag(SPV_ERROR_INVALID_ID, inst)
<< vstate.VkErrorID(10684)
<< "Invalid explicit layout decorations on type "
<< vstate.getIdName(type_id) << ", the "
<< spvtools::StorageClassToString(sc)
<< " storage class has an explicit layout from the "
<< vstate.SpvDecorationString(dec) << " decoration";
}
}
}
switch (type_inst->opcode()) {
case spv::Op::OpTypeStruct:
for (uint32_t i = 1; i < type_inst->operands().size(); i++) {
if (auto error = CheckNoLayout(
inst, type_inst->GetOperandAs<uint32_t>(i), sc)) {
return error;
}
}
break;
case spv::Op::OpTypeRuntimeArray:
case spv::Op::OpTypeArray:
if (auto error = CheckNoLayout(
inst, type_inst->GetOperandAs<uint32_t>(1u), sc)) {
return error;
}
break;
case spv::Op::OpTypePointer: {
auto ptr_sc = type_inst->GetOperandAs<spv::StorageClass>(1u);
if (auto error = CheckNoLayout(
inst, type_inst->GetOperandAs<uint32_t>(2u), ptr_sc)) {
return error;
}
break;
}
default:
break;
}
no_layout_cache_.insert(type_id);
return SPV_SUCCESS;
}
// Returns true if type_id contains a matrix. Only looks through arrays.
bool ContainsMatrix(uint32_t type_id) {
const auto* type_inst = vstate.FindDef(type_id);
switch (type_inst->opcode()) {
case spv::Op::OpTypeMatrix:
return true;
case spv::Op::OpTypeArray:
case spv::Op::OpTypeRuntimeArray:
return ContainsMatrix(type_inst->GetOperandAs<uint32_t>(1u));
default:
break;
}
return false;
}
// Gets the value for an id-based layout decoration (e.g. ArrayStrideIdEXT and
// OffsetIdEXT). Returns max uint32_t if the value cannot be evaluated (e.g.
// spec constant).
uint32_t GetIdDecorationValue(uint32_t id) {
const auto* inst = vstate.FindDef(id);
if (!spvOpcodeIsConstant(inst->opcode())) {
return std::numeric_limits<uint32_t>::max();
}
uint64_t value = 0;
if (vstate.EvalConstantValUint64(id, &value)) {
return static_cast<uint32_t>(value);
}
return std::numeric_limits<uint32_t>::max();
}
// Returns whether an array stride decoration is present on the array and its
// value.
std::pair<bool, uint32_t> GetArrayStride(uint32_t array_id) {
uint32_t stride = std::numeric_limits<uint32_t>::max();
bool has_stride = false;
for (auto& d : vstate.id_decorations(array_id)) {
if (d.dec_type() == spv::Decoration::ArrayStride) {
stride = d.params()[0];
has_stride = true;
break;
} else if (d.dec_type() == spv::Decoration::ArrayStrideIdEXT) {
stride = GetIdDecorationValue(d.params()[0]);
has_stride = true;
break;
}
}
return std::make_pair(has_stride, stride);
}
// Gets the offset value from an offset decoration.
uint32_t GetOffset(spv::Decoration dec, uint32_t param) {
// param is a literal value
if (dec == spv::Decoration::Offset) {
return param;
}
// param is an id
return GetIdDecorationValue(param);
}
// Returns true if value is aligned to align.
bool IsAlignedTo(uint32_t value, uint32_t align) {
if (align == 0) return value == 0;
return (value % align) == 0;
}
// Rounds up value to next multiple of align.
uint32_t AlignTo(uint32_t value, uint32_t align) {
return (value + align - 1) & ~(align - 1);
}
// A member is defined to improperly straddle if either of the following are
// true:
// - It is a vector with total size less than or equal to 16 bytes, and has
// Offset decorations placing its first byte at F and its last byte at L,
// where floor(F / 16) != floor(L / 16).
// - It is a vector with total size greater than 16 bytes and has its Offset
// decorations placing its first byte at a non-integer multiple of 16.
bool HasImproperStraddle(uint32_t offset, uint32_t size) {
const auto F = offset;
const auto L = offset + size - 1;
if (size <= 16) {
if ((F >> 4) != (L >> 4)) return true;
} else {
if (F % 16 != 0) return true;
}
return false;
}
// Returns the alignment for type_id for the given layout rules.
uint32_t GetAlign(uint32_t type_id, LayoutMode mode,
const MatrixConstraints& matrix_constraints,
bool allow_relaxed = true) {
const auto* type_inst = vstate.FindDef(type_id);
uint32_t align = 1;
switch (type_inst->opcode()) {
case spv::Op::OpTypeSampledImage:
case spv::Op::OpTypeSampler:
case spv::Op::OpTypeImage:
if (vstate.HasCapability(spv::Capability::BindlessTextureNV)) {
return vstate.samplerimage_variable_address_mode() / 8;
}
if (type_inst->opcode() == spv::Op::OpTypeSampler) {
return vstate.options()->sampler_descriptor_layout.alignment;
}
if (type_inst->opcode() == spv::Op::OpTypeImage) {
return vstate.options()->image_descriptor_layout.alignment;
}
break;
case spv::Op::OpTypeBufferEXT:
case spv::Op::OpTypeAccelerationStructureKHR:
return vstate.options()->buffer_descriptor_layout.alignment;
case spv::Op::OpTypeInt:
case spv::Op::OpTypeFloat:
return type_inst->GetOperandAs<uint32_t>(1u) / 8;
case spv::Op::OpTypeVector:
case spv::Op::OpTypeVectorIdEXT: {
const auto ele_id = type_inst->GetOperandAs<uint32_t>(1u);
const auto num_eles = vstate.GetDimension(type_id);
align = GetAlign(ele_id, mode, {});
// Relaxed layout only applies to vectors as direct members of structs.
// Arrays and matrices are not relaxed.
if (mode == LayoutMode::kScalar ||
(allow_relaxed && vstate.IsRelaxedBlockLayout())) {
return align;
}
return align * ((num_eles == 3 || num_eles > 4) ? 4 : num_eles);
}
case spv::Op::OpTypeMatrix:
if (mode == LayoutMode::kScalar) {
const auto* vec_inst =
vstate.FindDef(type_inst->GetOperandAs<uint32_t>(1u));
const auto ele_id = vec_inst->GetOperandAs<uint32_t>(1u);
return GetAlign(ele_id, mode, {});
}
if (matrix_constraints.col_major) {
align = GetAlign(type_inst->GetOperandAs<uint32_t>(1u), mode, {},
/* allow_relaxed = */ false);
} else {
// A row-major matrix of C columns has a base alignment equal to the
// base alignment of a vector of C matrix components.
const auto num_cols = type_inst->GetOperandAs<uint32_t>(2u);
const auto* col_inst =
vstate.FindDef(type_inst->GetOperandAs<uint32_t>(1u));
const auto ele_id = col_inst->GetOperandAs<uint32_t>(1u);
align = GetAlign(ele_id, mode, {});
// The equivalent vector may not exist so we replicate the vector rule
// here.
if (mode != LayoutMode::kScalar) {
align = align * (num_cols == 3 ? 4 : num_cols);
}
}
if (mode == LayoutMode::kExtended) {
align = AlignTo(align, 16u);
}
return align;
case spv::Op::OpTypeArray:
case spv::Op::OpTypeRuntimeArray:
align = GetAlign(type_inst->GetOperandAs<uint32_t>(1u), mode,
matrix_constraints, /* allow_relaxed = */ false);
if (mode == LayoutMode::kExtended) {
align = AlignTo(align, 16u);
}
return align;
case spv::Op::OpTypeStruct:
for (uint32_t i = 1; i < type_inst->operands().size(); i++) {
const auto member_id = type_inst->GetOperandAs<uint32_t>(i);
MatrixConstraints mat_constraints;
GetMatrixConstraints(type_id, i - 1, &mat_constraints);
align = std::max(align, GetAlign(member_id, mode, mat_constraints));
if (mode == LayoutMode::kExtended) {
align = AlignTo(align, 16u);
}
}
return align;
case spv::Op::OpTypePointer:
case spv::Op::OpTypeUntypedPointerKHR:
return vstate.pointer_size_and_alignment();
default:
break;
}
assert(0 && "unhandled type");
return 1;
}
// Returns the size of the given type.
uint32_t GetSize(uint32_t type_id,
const MatrixConstraints& matrix_constraints) {
const auto* type_inst = vstate.FindDef(type_id);
switch (type_inst->opcode()) {
case spv::Op::OpTypeSampledImage:
case spv::Op::OpTypeSampler:
case spv::Op::OpTypeImage:
if (vstate.HasCapability(spv::Capability::BindlessTextureNV)) {
return vstate.samplerimage_variable_address_mode() / 8;
}
if (type_inst->opcode() == spv::Op::OpTypeSampler) {
return vstate.options()->sampler_descriptor_layout.size;
}
if (type_inst->opcode() == spv::Op::OpTypeImage) {
return vstate.options()->image_descriptor_layout.size;
}
break;
case spv::Op::OpTypeBufferEXT:
case spv::Op::OpTypeAccelerationStructureKHR:
return vstate.options()->buffer_descriptor_layout.size;
case spv::Op::OpTypeInt:
case spv::Op::OpTypeFloat:
return type_inst->GetOperandAs<uint32_t>(1u) / 8;
case spv::Op::OpTypeVector:
case spv::Op::OpTypeVectorIdEXT: {
const auto ele_id = type_inst->GetOperandAs<uint32_t>(1u);
const auto num_eles = vstate.GetDimension(type_id);
return GetSize(ele_id, {}) * num_eles;
}
case spv::Op::OpTypeArray: {
const auto count_id = type_inst->GetOperandAs<uint32_t>(2u);
uint64_t count = 0;
if (!vstate.EvalConstantValUint64(count_id, &count)) {
return 0;
}
const auto [has_stride, stride] = GetArrayStride(type_id);
const auto ele_size =
GetSize(type_inst->GetOperandAs<uint32_t>(1u), matrix_constraints);
// uint32 max is a marker for unevaluatable.
if (stride == std::numeric_limits<uint32_t>::max()) {
return ele_size;
}
return (static_cast<uint32_t>(count) - 1) * stride + ele_size;
}
case spv::Op::OpTypeRuntimeArray:
return 0;
case spv::Op::OpTypeMatrix: {
const auto num_cols = type_inst->GetOperandAs<uint32_t>(2u);
if (matrix_constraints.col_major) {
return num_cols * matrix_constraints.stride;
} else {
const auto* col_inst =
vstate.FindDef(type_inst->GetOperandAs<uint32_t>(1u));
const auto ele_id = col_inst->GetOperandAs<uint32_t>(1u);
const auto num_rows = col_inst->GetOperandAs<uint32_t>(2u);
return (num_rows - 1) * matrix_constraints.stride +
num_cols * GetSize(ele_id, {});
}
}
case spv::Op::OpTypeStruct: {
const auto& members = GetStructMembers(type_id);
if (members.empty()) return 0;
const auto& last = members.back();
if (last.offset == std::numeric_limits<uint32_t>::max()) {
return 0;
}
return last.offset +
GetSize(type_inst->GetOperandAs<uint32_t>(last.index + 1),
last.matrix_constraints);
}
case spv::Op::OpTypePointer:
case spv::Op::OpTypeUntypedPointerKHR:
return vstate.pointer_size_and_alignment();
default:
break;
}
assert(0 && "unhandled type");
return 0;
}
// Returns true if type_id has a matrix and populates matrix_constraints.
bool GetMatrixConstraints(uint32_t type_id, uint32_t index,
MatrixConstraints* matrix_constraints) {
bool has_matrix = false;
auto member_decorations = vstate.id_member_decorations(type_id, index);
for (auto decoration = member_decorations.begin;
decoration != member_decorations.end; ++decoration) {
if (decoration->dec_type() == spv::Decoration::ColMajor ||
decoration->dec_type() == spv::Decoration::RowMajor) {
has_matrix = true;
matrix_constraints->col_major =
decoration->dec_type() == spv::Decoration::ColMajor;
}
if (decoration->dec_type() == spv::Decoration::MatrixStride) {
matrix_constraints->stride = decoration->params()[0];
}
}
return has_matrix;
}
// Gathers (and caches) structure members and their decorations.
const std::vector<MemberInfo>& GetStructMembers(uint32_t type_id) {
if (struct_members_.count(type_id)) {
return struct_members_[type_id];
}
const auto* type_inst = vstate.FindDef(type_id);
std::vector<MemberInfo> member_info;
member_info.reserve(type_inst->operands().size() - 1);
for (uint32_t i = 1; i < type_inst->operands().size(); i++) {
auto member_idx = i - 1;
auto member_decorations =
vstate.id_member_decorations(type_id, member_idx);
member_info.push_back(MemberInfo{member_idx});
auto& member = member_info.back();
member.has_matrix =
GetMatrixConstraints(type_id, member_idx, &member.matrix_constraints);
for (auto decoration = member_decorations.begin;
decoration != member_decorations.end; ++decoration) {
switch (decoration->dec_type()) {
case spv::Decoration::Offset:
case spv::Decoration::OffsetIdEXT:
member.has_offset = true;
member.offset =
GetOffset(decoration->dec_type(), decoration->params()[0]);
break;
default:
break;
}
}
}
// Sort by offset value.
std::stable_sort(member_info.begin(), member_info.end(),
[](const MemberInfo& lhs, const MemberInfo& rhs) {
return lhs.offset < rhs.offset;
});
struct_members_[type_id] = std::move(member_info);
return struct_members_[type_id];
}
// Returns some common messaging to improve diagnostics.
std::string CommonError(const Instruction* inst,
spv::StorageClass storage_class, LayoutMode mode) {
std::string s;
std::ostringstream str(s);
str << " Instantiated via " << vstate.getIdName(inst->id()) << " in the "
<< spvtools::StorageClassToString(storage_class)
<< " storage class using " << mode << " layout rules.";
if (mode != LayoutMode::kScalar) {
if (storage_class == spv::StorageClass::Workgroup) {
str << vstate.MissingFeature(
"workgroupMemoryExplicitLayoutScalarBlockLayout feature",
"--workgroup-scalar-block-layout", true);
} else if (!vstate.IsRelaxedBlockLayout()) {
str << vstate.MissingFeature("VK_KHR_relaxed_block_layout extension",
"--relax-block-layout", true);
} else if (storage_class == spv::StorageClass::Uniform &&
mode != LayoutMode::kStandard) {
str << vstate.MissingFeature("uniformBufferStandardLayout feature",
"--uniform-buffer-standard-layout", true);
} else {
str << vstate.MissingFeature("scalarBlockLayout feature",
"--scalar-block-layout", true);
}
}
return str.str();
}
// Checks struct layouts
// * Each member must
// * Have an offset decoration
// * If the member is a matrix or array(s) of matrices
// * Must have majorness and matrix stride decorations
// * If member is a runtime array, it must be last by offset
// * Offset must be aligned
// * Total offset must be aligned
// * Offset + size < next elements offset
// * If member is a vector, it must have a valid straddle
// * The member has a valid layout
spv_result_t CheckStructLayout(const Instruction* inst, uint32_t type_id,
spv::StorageClass storage_class,
LayoutMode mode, uint32_t incoming_offset,
const MatrixConstraints& matrix_constraints) {
const auto* type_inst = vstate.FindDef(type_id);
const auto& member_info = GetStructMembers(type_id);
for (auto& member : member_info) {
const auto member_id =
type_inst->GetOperandAs<uint32_t>(member.index + 1);
if (!member.has_offset) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Structure member " << member.index
<< " must be explicitly laid out with Offset or OffsetIdEXT "
"decorations."
<< CommonError(inst, storage_class, mode);
}
if (ContainsMatrix(member_id)) {
if (!member.has_matrix) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Structure member " << member.index
<< " containing a matrix must be explicitly laid out "
"with RowMajor or ColMajor decorations."
<< CommonError(inst, storage_class, mode);
} else if (member.matrix_constraints.stride == 0) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Structure member " << member.index
<< " containing a matrix must be explicitly laid out "
"with MatrixStride decorations."
<< CommonError(inst, storage_class, mode);
}
}
}
uint32_t next_offset = 0;
uint32_t ordered_index = 0;
for (const auto& member : member_info) {
auto member_idx = member.index;
auto offset = member.offset;
// We have no information
if (offset == std::numeric_limits<uint32_t>::max()) {
continue;
}
auto member_id = type_inst->GetOperandAs<uint32_t>(member_idx + 1);
auto member_inst = vstate.FindDef(member_id);
if (member_inst->opcode() == spv::Op::OpTypeRuntimeArray &&
ordered_index != member_info.size() - 1) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< vstate.VkErrorID(4680)
<< "Structure has a runtime array at offset " << offset
<< ", but other members at larger offsets."
<< CommonError(inst, storage_class, mode);
}
ordered_index++;
MatrixConstraints mat_constraints = matrix_constraints;
if (member.has_matrix) {
mat_constraints = member.matrix_constraints;
}
uint32_t align = GetAlign(member_id, mode, mat_constraints);
if (!IsAlignedTo(offset, align)) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Structure member " << member_idx << " at offset " << offset
<< " is not aligned to " << align << "."
<< CommonError(inst, storage_class, mode);
}
if (!IsAlignedTo(offset + incoming_offset, align)) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Structure member " << member_idx << " at offset " << offset
<< " plus incoming offset " << incoming_offset
<< " is not aligned to " << align << "."
<< CommonError(inst, storage_class, mode);
}
if (offset < next_offset) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Structure member " << member_idx << " at offset " << offset
<< " overlaps previous member ending at offset "
<< next_offset - 1 << "."
<< CommonError(inst, storage_class, mode);
}
if (mode != LayoutMode::kScalar && vstate.IsRelaxedBlockLayout()) {
if (member_inst->opcode() == spv::Op::OpTypeVector ||
member_inst->opcode() == spv::Op::OpTypeVectorIdEXT) {
uint32_t size = GetSize(member_id, {});
if (HasImproperStraddle(incoming_offset + offset, size)) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Structure member " << member_idx
<< ": Vector has improper straddle due to offset "
<< incoming_offset + offset << "."
<< CommonError(inst, storage_class, mode);
}
}
}
if (auto error = CheckLayout(inst, member_id, storage_class, mode,
incoming_offset + offset, mat_constraints)) {
return error;
}
uint32_t size = GetSize(member_id, mat_constraints);
next_offset = size + offset;
if (mode != LayoutMode::kScalar &&
(member_inst->opcode() == spv::Op::OpTypeArray ||
member_inst->opcode() == spv::Op::OpTypeStruct)) {
next_offset = AlignTo(next_offset, align);
}
}
return SPV_SUCCESS;
}
// Checks array layouts
// * Arrays must have stride decoration
// * Stride must be non-zero
// * Stride must be aligned
// * Stride must be greater or equal to element size
// * Elements have valid layout
spv_result_t CheckArrayLayout(const Instruction* inst, uint32_t type_id,
spv::StorageClass storage_class,
LayoutMode mode, uint32_t incoming_offset,
const MatrixConstraints& matrix_constraints) {
const auto* type_inst = vstate.FindDef(type_id);
auto [has_stride, stride] = GetArrayStride(type_id);
if (!has_stride) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Array must be explicitly laid out with ArrayStride or "
"ArrayStrideIdEXT decorations."
<< CommonError(inst, storage_class, mode);
}
uint32_t ele_id = type_inst->GetOperandAs<uint32_t>(1u);
uint32_t ele_size = GetSize(ele_id, matrix_constraints);
uint32_t align = GetAlign(type_id, mode, matrix_constraints);
if (stride == 0) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Array must not have a stride of 0."
<< CommonError(inst, storage_class, mode);
}
// uint32 max stride is unevaluatable (e.g. spec constant).
if (stride == std::numeric_limits<uint32_t>::max()) {
return SPV_SUCCESS;
}
if (!IsAlignedTo(stride, align)) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Array stride " << stride << " must satisfy alignment " << align
<< "." << CommonError(inst, storage_class, mode);
}
if (stride != std::numeric_limits<uint32_t>::max() && stride < ele_size) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Array stride " << stride
<< " is smaller than element type size " << ele_size << "."
<< CommonError(inst, storage_class, mode);
}
uint32_t num_elements = 0;
if (type_inst->opcode() == spv::Op::OpTypeArray) {
uint64_t count = 0;
if (vstate.EvalConstantValUint64(type_inst->GetOperandAs<uint32_t>(2u),
&count)) {
num_elements = static_cast<uint32_t>(count);
}
}
num_elements = std::max(1u, num_elements);
std::vector<bool> seen(16, false);
for (uint32_t i = 0; i < num_elements; ++i) {
uint32_t next_offset = i * stride + incoming_offset;
// Stop checking if offsets repeat in terms of 16-byte multiples.
if (seen[next_offset % 16]) {
break;
}
if (auto error = CheckLayout(inst, ele_id, storage_class, mode,
next_offset, matrix_constraints)) {
return error;
}
seen[next_offset % 16] = true;
}
return SPV_SUCCESS;
}
// Checks the layout matrices
// * Stride must be a multiple of align
// * Stride must be greater or equal to minor size
spv_result_t CheckMatrixLayout(const Instruction* inst, uint32_t type_id,
spv::StorageClass storage_class,
LayoutMode mode,
const MatrixConstraints& matrix_constraints) {
// We already checked that any struct containing a matrix has a non-zero
// stride so if we have 0 stride here then it will come from the result
// of an access chain or other instruction. Other rules are meant to
// catch any misuse we can skip it here.
if (matrix_constraints.stride == 0) {
return SPV_SUCCESS;
}
const auto* type_inst = vstate.FindDef(type_id);
const auto align = GetAlign(type_id, mode, matrix_constraints);
if (!IsAlignedTo(matrix_constraints.stride, align)) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Matrix with a stride " << matrix_constraints.stride
<< " not satisfying alignment to " << align << "."
<< CommonError(inst, storage_class, mode) << "\n";
}
const auto ele_id = type_inst->GetOperandAs<uint32_t>(1u);
uint32_t size = 0;
if (matrix_constraints.col_major) {
size = GetSize(ele_id, {});
} else {
// Element size is # cols * ele size.
const auto* ele_inst = vstate.FindDef(ele_id);
const auto scalar_id = ele_inst->GetOperandAs<uint32_t>(1u);
size = GetSize(scalar_id, {}) * type_inst->GetOperandAs<uint32_t>(2u);
}
if (matrix_constraints.stride < size) {
return vstate.diag(SPV_ERROR_INVALID_ID, type_inst)
<< "Matrix stride " << matrix_constraints.stride
<< " is smaller than column size " << size << "."
<< CommonError(inst, storage_class, mode);
}
return SPV_SUCCESS;
}
// Returns true if type_id satisfies the given layout rules.
spv_result_t CheckLayout(const Instruction* inst, uint32_t type_id,
spv::StorageClass storage_class, LayoutMode mode,
uint32_t incoming_offset,
const MatrixConstraints& matrix_constraints) {
if (vstate.options()->skip_block_layout) {
return SPV_SUCCESS;
}
LayoutKey key{type_id, mode, incoming_offset, matrix_constraints};
if (layout_cache_.count(key)) {
return SPV_SUCCESS;
}
const auto* type_inst = vstate.FindDef(type_id);
switch (type_inst->opcode()) {
case spv::Op::OpTypeStruct:
if (auto error =
CheckStructLayout(inst, type_id, storage_class, mode,
incoming_offset, matrix_constraints)) {
return error;
}
break;
case spv::Op::OpTypeArray:
case spv::Op::OpTypeRuntimeArray:
if (auto error =
CheckArrayLayout(inst, type_id, storage_class, mode,
incoming_offset, matrix_constraints)) {
return error;
}
break;
case spv::Op::OpTypeMatrix:
if (auto error = CheckMatrixLayout(inst, type_id, storage_class, mode,
matrix_constraints)) {
return error;
}
break;
default:
break;
}
layout_cache_.insert(key);
return SPV_SUCCESS;
}
// Performs a single pass over the IR and for each memory reference determines
// what validation is necessary.
spv_result_t Run() {
if (!spvIsVulkanEnv(vstate.context()->target_env)) {
return SPV_SUCCESS;
}
for (const auto& inst : vstate.ordered_instructions()) {
MemoryReference reference;
if (!GetMemoryReference(&inst, &reference)) {
continue;
}
// Descriptor arrays shouldn't have a stride. Check for that here since
// most descriptors require a layout.
if (reference.descriptor_array_id != 0) {
const bool array_stride = vstate.HasDecoration(
reference.descriptor_array_id, spv::Decoration::ArrayStride);
const bool array_stride_id = vstate.HasDecoration(
reference.descriptor_array_id, spv::Decoration::ArrayStrideIdEXT);
if (array_stride || array_stride_id) {
return vstate.diag(SPV_ERROR_INVALID_ID, &inst)
<< vstate.VkErrorID(10684)
<< "Invalid explicit layout decorations on type "
<< vstate.getIdName(reference.descriptor_array_id) << ", the "
<< spvtools::StorageClassToString(reference.storage_class)
<< " storage class has an explicit layout from the "
<< vstate.SpvDecorationString(
array_stride ? spv::Decoration::ArrayStride
: spv::Decoration::ArrayStrideIdEXT)
<< " decoration";
}
}
// Untyped pointers require a layout. Workgroup variables must be blocks
// to have a layout.
if (reference.untyped &&
reference.storage_class == spv::StorageClass::Workgroup &&
inst.opcode() == spv::Op::OpUntypedVariableKHR &&
(vstate.GetIdOpcode(reference.type_id) != spv::Op::OpTypeStruct ||
!vstate.HasDecoration(reference.type_id, spv::Decoration::Block))) {
return vstate.diag(SPV_ERROR_INVALID_ID, &inst)
<< vstate.VkErrorID(10684)
<< "Untyped variables in Workgroup storage class must be "
"block-decorated structs";
}
if (reference.requirement == LayoutRequirement::kRequired) {
if (auto error =
CheckLayout(&inst, reference.type_id, reference.storage_class,
reference.layout, 0, {})) {
return error;
}
} else if (reference.requirement == LayoutRequirement::kProhibited) {
if (auto error = CheckNoLayout(&inst, reference.type_id,
reference.storage_class)) {
return error;
}
}
}
return SPV_SUCCESS;
}
};
} // namespace
spv_result_t ValidateExplicitLayout(ValidationState_t& vstate) {
return Impl{vstate}.Run();
}
} // namespace val
} // namespace spvtools