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.cpp
layers/state_tracker/shader_module.cpp
VkShaderModule
objectlayers/state_tracker/shader_stage_state.cpp
layers/core_checks/cc_spirv.cpp
layers/vulkan/generated/spirv_validation_helper.cpp
vk.xml
related to SPIR-Vlayers/vulkan/generated/spirv_grammar_helper.cpp
All 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
EntryPoints
ShaderModule
not related to shader stage validationSPIR-V Module
SPIRV_MODULE_STATE
EntryPoint
objects, validates what we canuint32_t
words)Shader Module
and Shader Object
VkShaderModule
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
)ShaderModule
ShaderModule
isPipeline
Shader 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_STATE
Pipeline Library
it might need to wait until linkingEntryPoint
ShaderModuleIdentifier
There 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 Variable
Image operation
can point to 2 different Variables
Any 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_SAMPLER
VK_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