Shader Validation

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.

Standalone VUs with spirv-val

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.

spirv-opt

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
  • 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
  • GPU-VA

Different sections

The code is currently split up into the following main sections

  • layers/shader_instruction.cpp
    • This contains information about each SPIR-V instruction.
  • layers/shader_module.cpp
    • This contains information about the VkShaderModule object
  • layers/shader_validation.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
  • layers/vulkan/generated/spirv_grammar_helper.cpp
    • This is a general util file that is generated from the SPIR-V grammar

Types of Shader Validation

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

Design details

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
  • SPIR-V Module
    • SPIRV_MODULE_STATE
    • This object takes in SPIR-V, parses it, creates EntryPoint objects, validates what we can
    • contains SPIR-V instructions (in an array of uint32_t words)
    • knows the relationship between instructions
  • Shader Module and Shader Object
    • VkShaderModule object (SHADER_MODULE_STATE) or VkShaderEXT object (SHADER_OBJECT_STATE)
    • can hold a SPIR-V module reference
      • Pipeline 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
  • Pipeline
    • contains 1 or more Shader Module object
    • decides both which Shader Module and EntryPoint are used
    • has other state not known if validating just the shader object

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
  • 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

Variables

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 or User Defined variables

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 same Variable
  • 2 Image operation can point to 2 different Variables

Image Accesses

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

  • If it has OpTypeSampledImage, there is no way for it to be part of a SAMPLER/SAMPLED_IMAGE combo
  • If it has a OpTypeImage or OpTypeSampler, we need to know if they are accessed together
    • This means the the ValidateDescriptor logic needs to know every OpTypeSampler variable accessed together with a OpTypeImage variable
    • There is no case where only a OpTypeSampler variable can be used by itself, so no need to track it the other way

Atomics

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