// 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 #include #include #include #include #include #include #include #include #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. // Only used for error messages 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 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::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> 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(key.layout), key.incoming_offset, key.matrix_constraints.stride, key.matrix_constraints.col_major); } }; // Caches valid results of CheckLayout calls. std::unordered_set 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(1u); const bool is_descriptor_heap = vstate.IsDescriptorHeapBaseVariable(inst); reference->storage_class = sc; reference->type_id = type_inst->GetOperandAs(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(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(1u) == spv::StorageClass::PhysicalStorageBuffer) { reference->storage_class = spv::StorageClass::PhysicalStorageBuffer; reference->type_id = type_inst->GetOperandAs(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(1u); reference->storage_class = sc; uint32_t pointee_ty_id = type_inst->GetOperandAs(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(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(2); break; case spv::Op::OpLoad: if (vstate.GetIdOpcode(vstate.GetOperandTypeId(inst, 2)) == spv::Op::OpTypeUntypedPointerKHR) { const auto ptr_id = inst->GetOperandAs(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(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(3))->type_id(); data_ty_id = inst->GetOperandAs(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(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(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; } else if (inst->opcode() == spv::Op::OpAbortKHR) { reference->type_id = inst->GetOperandAs(0u); // Abort messages doesn't have a storage class reference->storage_class = spv::StorageClass::Max; // Currently not specified well // https://gitlab.khronos.org/spirv/SPIR-V/-/work_items/962 reference->layout = vstate.options()->scalar_block_layout ? LayoutMode::kScalar : LayoutMode::kStandard; reference->requirement = LayoutRequirement::kRequired; 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(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(i), sc)) { return error; } } break; case spv::Op::OpTypeRuntimeArray: case spv::Op::OpTypeArray: if (auto error = CheckNoLayout( inst, type_inst->GetOperandAs(1u), sc)) { return error; } break; case spv::Op::OpTypePointer: { auto ptr_sc = type_inst->GetOperandAs(1u); if (auto error = CheckNoLayout( inst, type_inst->GetOperandAs(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(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::max(); } uint64_t value = 0; if (vstate.EvalConstantValUint64(id, &value)) { return static_cast(value); } return std::numeric_limits::max(); } // Returns whether an array stride decoration is present on the array and its // value. std::pair GetArrayStride(uint32_t array_id) { uint32_t stride = std::numeric_limits::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; } // Used to map the option alignment to the Vulkan VUID uint32_t GetAlignVkErrorId(spv::Op opcode) { if (opcode == spv::Op::OpTypeSampler) { return 11476; } else if (opcode == spv::Op::OpTypeSampledImage || opcode == spv::Op::OpTypeImage) { return 11477; } else if (opcode == spv::Op::OpTypeBufferEXT) { return 11478; } else if (opcode == spv::Op::OpTypeAccelerationStructureKHR) { return 11479; } else if (opcode == spv::Op::OpTypeTensorARM) { return 11480; } return 0; } // 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(1u) / 8; case spv::Op::OpTypeVector: case spv::Op::OpTypeVectorIdEXT: { const auto ele_id = type_inst->GetOperandAs(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(1u)); const auto ele_id = vec_inst->GetOperandAs(1u); return GetAlign(ele_id, mode, {}); } if (matrix_constraints.col_major) { align = GetAlign(type_inst->GetOperandAs(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(2u); const auto* col_inst = vstate.FindDef(type_inst->GetOperandAs(1u)); const auto ele_id = col_inst->GetOperandAs(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(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(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(1u) / 8; case spv::Op::OpTypeVector: case spv::Op::OpTypeVectorIdEXT: { const auto ele_id = type_inst->GetOperandAs(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(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(1u), matrix_constraints); // uint32 max is a marker for unevaluatable. if (stride == std::numeric_limits::max()) { return ele_size; } return (static_cast(count) - 1) * stride + ele_size; } case spv::Op::OpTypeRuntimeArray: return 0; case spv::Op::OpTypeMatrix: { const auto num_cols = type_inst->GetOperandAs(2u); if (matrix_constraints.col_major) { return num_cols * matrix_constraints.stride; } else { const auto* col_inst = vstate.FindDef(type_inst->GetOperandAs(1u)); const auto ele_id = col_inst->GetOperandAs(1u); const auto num_rows = col_inst->GetOperandAs(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::max()) { return 0; } return last.offset + GetSize(type_inst->GetOperandAs(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& 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 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()); // OpAbortKHR is an example where there is not storage class if (storage_class != spv::StorageClass::Max) { str << " in the " << spvtools::StorageClassToString(storage_class) << " storage class"; } str << " 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(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::max()) { continue; } auto member_id = type_inst->GetOperandAs(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) << vstate.VkErrorID(GetAlignVkErrorId(member_inst->opcode())) << "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) << vstate.VkErrorID(GetAlignVkErrorId(member_inst->opcode())) << "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(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::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::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(2u), &count)) { num_elements = static_cast(count); } } num_elements = std::max(1u, num_elements); std::vector 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(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(1u); size = GetSize(scalar_id, {}) * type_inst->GetOperandAs(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