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
- Specialization constants
- This
spirv-opt
pass is used to inject the constants from the pipeline layout. - Some checks require the runtime spec constant values
- This
- Flatten OpGroupDecorations
- Detects if group decorations were used; however, group decorations were deprecated early on in the development of the SPIR-v specification.
- Debug Printf
- instruments the shaders to write the
printf
values out to a buffer
- instruments the shaders to write the
The code is currently split up into the following main sections
layers/state_tracker/shader_instruction.cpp
- This contains information about each SPIR-V instruction.
layers/state_tracker/shader_module.cpp
- This contains information about the
VkShaderModule
object
- This contains information about the
layers/state_tracker/shader_stage_state.cpp
- This contains information about the shader stage, this can be either from a Pipeline and/or Shader Object
layers/core_checks/cc_spirv.cpp
- This takes the following above and does the actual validation. All errors are produced here.
layers/vulkan/generated/spirv_validation_helper.cpp
- This is generated file provides a way to generate checks for things found in the
vk.xml
related to SPIR-V
- This is generated file provides a way to generate checks for things found in the
layers/vulkan/generated/spirv_grammar_helper.cpp
- This is a general util file that is generated from the SPIR-V grammar
All Shader Validation can be broken into 4 types of checks
- SPIR-V with runtime properties
- Things like features and limits
- Shader interface
- Ex. going between a Vertex and Fragment shader
- Interaction with Pipeline creation structs
- Vertex input, fragment output, etc
- Draw time
- making sure bound descriptor matches up what is being touched
When dealing with shader validation there are a few concepts to understand and not confuse
EntryPoints
- Tied to a shader stage (fragment, vertex, etc)
- Knows which variables and instructions are touched in stage
- There might be things in a
ShaderModule
not related to shader stage validation
- There might be things in a
SPIR-V Module
SPIRV_MODULE_STATE
- This object takes in SPIR-V, parses it, creates
EntryPoint
objects, validates what we can- We do validation first as sometimes a bad SPIR-V can crash a driver
- can contain multiple EntryPoints
- contains SPIR-V instructions (in an array of
uint32_t
words) - knows the relationship between instructions
Shader Module
andShader Object
VkShaderModule
object (SHADER_MODULE_STATE
) orVkShaderEXT
object (SHADER_OBJECT_STATE
)- can hold a
SPIR-V module
referencePipeline Library
(GPL) (VK_EXT_graphics_pipeline_library
)- part of a pipeline that can be reused
ShaderModuleIdentifier
(VK_EXT_shader_module_identifier
)- lets app use a hash instead of having the driver re-create the
ShaderModule
- not possible to validate as the VVL don't know what the
ShaderModule
is
- lets app use a hash instead of having the driver re-create the
Pipeline
- contains 1 or more
Shader Module
object - decides both which
Shader Module
andEntryPoint
are used - has other state not known if validating just the shader object
- contains 1 or more
When dealing with validation, it is important to know what should be validated, and when.
If validation only cares about... :
- the SPIR-V itself, is mapped to the
SPIRV_MODULE_STATE
- if two stages interface, needs to be done when all stages are there
- For
Pipeline Library
it might need to wait until linking
- For
- descriptors variables, use
EntryPoint
- the stage of a shader module is always known, regardless of even using
ShaderModuleIdentifier
- Pipeline can have fields that are related to shaders, but don't actually require the SPIR-V
There are 2 types of Variables
Resource Interface
variables (mapped to descriptors)Stage Interface
variables (input and output between shader)- Can be either a
BuiltIn
orUser Defined
variables
- Can be either a
For each EntryPoint
we walk the functions and find all Variables
accessed (load, store, atomic).
Infomaration to note:
- It is possible to have multiple
EntryPoints
pointing to the same interface variable. - 2 different accesses (ex.
OpLoad
) can point to sameVariable
- 2
Image operation
can point to 2 differentVariables
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
andVK_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
- If it has
OpTypeSampledImage
, there is no way for it to be part of aSAMPLER
/SAMPLED_IMAGE
combo - If it has a
OpTypeImage
orOpTypeSampler
, we need to know if they are accessed together- This means the the
ValidateDescriptor
logic needs to know everyOpTypeSampler
variable accessed together with aOpTypeImage
variable - There is no case where only a
OpTypeSampler
variable can be used by itself, so no need to track it the other way
- This means the the
If 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