Shader validation is run at vkCreateShaderModule and vkCreate*Pipelines time. It makes sure both the SPIR-V is valid as well as the VkPipeline object interface with the shader. Note, this is all done on the CPU and different than GPU-Assisted Validation.
There are many VUID labeled as VUID-StandaloneSpirv-* and all the Built-In Variables VUIDs that can be validated on a single shader module and require no runtime information.
All of these validations are passed off to spirv-val in SPIRV-Tools.
To improve performance from run-to-run we make use of the VK_EXT_validation_cache extension.
During vkCreateShaderModule time the user can pass in a VkShaderModuleValidationCacheCreateInfoEXT object. Realistically, most apps will not do this, so the Validation Layers do this for apps. At vkCreateDevice/vkDestroyDevice we load/save a uint32_t hash of every VkShaderModuleCreateInfo::pCode. Before running validation on it, we check if it is a known/valid shader and if so, skip checking it again.
There are a few settings in spirv-val (ex. you can use --allow-localsizeid with VK_KHR_maintenance4) that change if the SPIR-V is legal or not. Because of this, we use both the SPIRV-Tools commit version, as well as the device features/extensions, to determine if the cache is valid or not. In practice, it will not matter too much for real apps as they normally don't toggle these few features on/off between runs.
There are a few special places where spirv-opt is run to reduce recreating work already done in SPIRV-Tools. These can be found by searching for RegisterPass in the code
Currently these are
spirv-opt pass is used to inject the constants from the pipeline layout.printf values out to a bufferThe code is currently split up into the following main sections
layers/state_tracker/shader_instruction.cpplayers/state_tracker/shader_module.cppVkShaderModule objectlayers/state_tracker/shader_stage_state.cpplayers/core_checks/cc_spirv.cpplayers/vulkan/generated/spirv_validation_helper.cppvk.xml related to SPIR-Vlayers/vulkan/generated/spirv_grammar_helper.cppAll Shader Validation can be broken into 4 types of checks
When dealing with shader validation there are a few concepts to understand and not confuse
EntryPointsShaderModule not related to shader stage validationSPIR-V ModuleSPIRV_MODULE_STATEEntryPoint objects, validates what we canuint32_t words)Shader Module and Shader ObjectVkShaderModule object (SHADER_MODULE_STATE) or VkShaderEXT object (SHADER_OBJECT_STATE)SPIR-V module referencePipeline Library (GPL) (VK_EXT_graphics_pipeline_library)ShaderModuleIdentifier (VK_EXT_shader_module_identifier)ShaderModuleShaderModule isPipelineShader Module objectShader Module and EntryPoint are usedWhen dealing with validation, it is important to know what should be validated, and when.
If validation only cares about... :
SPIRV_MODULE_STATEPipeline Library it might need to wait until linkingEntryPointShaderModuleIdentifierThere are 2 types of Variables
Resource Interface variables (mapped to descriptors)Stage Interface variables (input and output between shader)BuiltIn or User Defined variablesFor each EntryPoint we walk the functions and find all Variables accessed (load, store, atomic).
Infomaration to note:
EntryPoints pointing to the same interface variable.OpLoad) can point to same VariableImage operation can point to 2 different VariablesAny variable in a shader pointing to an Image is a Resource Interface variable. There are validation checks that need care only if the variable is accessed. This requires a OpImage* instruction to access the variable.
Most Accesses look like
OpImage* -> OpLoad -> OpAccessChain (optional) -> OpVariable
There are a few exceptions:
An Image Fetch has an OpImage prior to the OpLoad
OpImageFetch -> OpImage -> OpLoad -> OpVariable
Image Atomics use OpImageTexelPointer instead of OpLoad
OpAtomicLoad -> OpImageTexelPointer -> OpAccessChain (optional) -> OpVariable
The biggest thing to consider is using either a
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLERVK_DESCRIPTOR_TYPE_SAMPLER and VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE combo// VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
OpImageSampleExplicitLod -> OpLoad -> OpAccessChain (optional) -> OpVariable -> OpTypePointer -> OpTypeSampledImage
// VK_DESCRIPTOR_TYPE_SAMPLER and VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
OpImageSampleImplicitLod -> OpSampledImage -> OpTypeSampledImage
-> OpLoad -> OpAccessChain (optional) -> OpVariable (image)
-> OpLoad -> OpAccessChain (optional) -> OpVariable (sampler)
Both contain a OpTypeSampledImage, which is how we know a VkSampler is being used with the variable
But it is also possible to have the Image and Samplers mix and match
ImageAccess -> Image_0
-> Sampler_0
ImageAccess -> Image_0
-> Sampler_1
ImageAccess -> Image_0 (non-sampled access)
This is handled by having the Resource Interface variable track if it has a OpTypeSampledImage, OpTypeImage or OpTypeSampler
OpTypeSampledImage, there is no way for it to be part of a SAMPLER/SAMPLED_IMAGE comboOpTypeImage or OpTypeSampler, we need to know if they are accessed togetherValidateDescriptor logic needs to know every OpTypeSampler variable accessed together with a OpTypeImage variableOpTypeSampler variable can be used by itself, so no need to track it the other wayIf the Image Access is in a function, it might point to multiple OpVariable
There are 2 types of atomic accesses: “Image” and “Non-Image”
Image atomics are described above how they use OpImageTexelPointer instead of OpLoad
Non-Image atomics will look like
OpAtomicLoad -> OpAccessChain (optional) -> OpVariable