diff --git a/layer_gpu_support/README_LAYER.md b/layer_gpu_support/README_LAYER.md index 9bb6668..6b5c411 100644 --- a/layer_gpu_support/README_LAYER.md +++ b/layer_gpu_support/README_LAYER.md @@ -79,6 +79,21 @@ irrespective of other settings. } ``` +## Shaders and Pipelines + +The shaders and pipelines override allows some control over how the shader +compiler handles compilation tasks. + +#### Configuration options + +```jsonc +"shader": { + "disable_cache": false, // Disable use of binary caches + "disable_relaxed_precision": false, // Disable use of relaxed precision decoration + "enable_spirv_fuzz": false // Enable SPIR-V fuzzing to change binary hash +} +``` + - - - _Copyright © 2025, Arm Limited and contributors._ diff --git a/layer_gpu_support/layer_config.json b/layer_gpu_support/layer_config.json index 85c9781..9e05b60 100644 --- a/layer_gpu_support/layer_config.json +++ b/layer_gpu_support/layer_config.json @@ -22,5 +22,10 @@ } }, "queue": false + }, + "shader": { + "disable_cache": false, + "disable_relaxed_precision": false, + "enable_spirv_fuzz": false } } diff --git a/layer_gpu_support/source/CMakeLists.txt b/layer_gpu_support/source/CMakeLists.txt index 72f6662..bac7c56 100644 --- a/layer_gpu_support/source/CMakeLists.txt +++ b/layer_gpu_support/source/CMakeLists.txt @@ -46,6 +46,7 @@ add_library( instance.cpp layer_config.cpp layer_device_functions_dispatch.cpp + layer_device_functions_pipelines.cpp layer_device_functions_queue.cpp layer_device_functions_render_pass.cpp layer_device_functions_trace_rays.cpp diff --git a/layer_gpu_support/source/layer_config.cpp b/layer_gpu_support/source/layer_config.cpp index 29842c9..8209357 100644 --- a/layer_gpu_support/source/layer_config.cpp +++ b/layer_gpu_support/source/layer_config.cpp @@ -63,6 +63,7 @@ void LayerConfig::parse_serialization_options( bool s_stream_tx_pre = s_stream.at("transfer").at("pre"); bool s_stream_tx_post = s_stream.at("transfer").at("post"); + // Write after all options read from JSON so we know it parsed correctly serialize_queues = (!s_none) && (s_all || s_queue); serialize_dispatch_pre = (!s_none) && (s_all || s_stream_c_pre); serialize_dispatch_post = (!s_none) && (s_all || s_stream_c_post); @@ -86,6 +87,29 @@ void LayerConfig::parse_serialization_options( LAYER_LOG(" - Serialize transfer post: %d", serialize_transfer_post); } +/* See header for documentation. */ +void LayerConfig::parse_shader_options( + const json& config +) { + // Decode serialization state + json shader = config.at("shader"); + + bool disable_program_cache = shader.at("disable_cache"); + bool disable_program_relaxed_precision = shader.at("disable_relaxed_precision"); + bool enable_program_fuzz_spirv_hash = shader.at("enable_spirv_fuzz"); + + // Write after all options read from JSON so we know it parsed correctly + shader_disable_program_cache = disable_program_cache; + shader_disable_program_relaxed_precision = disable_program_relaxed_precision; + shader_enable_program_fuzz_spirv_hash = enable_program_fuzz_spirv_hash; + + LAYER_LOG("Layer shader configuration"); + LAYER_LOG("=========================="); + LAYER_LOG(" - Disable binary cache: %d", shader_disable_program_cache); + LAYER_LOG(" - Disable relaxed precision %d", shader_disable_program_relaxed_precision); + LAYER_LOG(" - Enable SPIR-V hash fuzzer: %d", shader_enable_program_fuzz_spirv_hash); +} + /* See header for documentation. */ LayerConfig::LayerConfig() { @@ -128,6 +152,16 @@ LayerConfig::LayerConfig() LAYER_ERR("Failed to read serialization config, using defaults"); LAYER_ERR("Error: %s", e.what()); } + + try + { + parse_shader_options(data); + } + catch(const json::out_of_range& e) + { + LAYER_ERR("Failed to read shader config, using defaults"); + LAYER_ERR("Error: %s", e.what()); + } } /* See header for documentation. */ @@ -183,3 +217,21 @@ bool LayerConfig::serialize_cmdstream_transfer_post() const { return serialize_transfer_post; } + +/* See header for documentation. */ +bool LayerConfig::shader_disable_cache() const +{ + return shader_disable_program_cache; +} + +/* See header for documentation. */ +bool LayerConfig::shader_disable_relaxed_precision() const +{ + return shader_disable_program_relaxed_precision; +} + +/* See header for documentation. */ +bool LayerConfig::shader_fuzz_spirv_hash() const +{ + return shader_enable_program_fuzz_spirv_hash; +} diff --git a/layer_gpu_support/source/layer_config.hpp b/layer_gpu_support/source/layer_config.hpp index f6e3ac0..196c7e2 100644 --- a/layer_gpu_support/source/layer_config.hpp +++ b/layer_gpu_support/source/layer_config.hpp @@ -49,6 +49,8 @@ class LayerConfig */ LayerConfig(); + // Config queries for serializer + /** * @brief True if config wants to serialize before compute workloads. */ @@ -94,9 +96,26 @@ class LayerConfig */ bool serialize_cmdstream_transfer_post() const; + // Config queries for shaders + + /** + * @brief True if config wants to disable shader caching. + */ + bool shader_disable_cache() const; + + /** + * @brief True if config wants to disable use of relaxed precision. + */ + bool shader_disable_relaxed_precision() const; + + /** + * @brief True if config wants to enable SPIR-V hash fuzzing. + */ + bool shader_fuzz_spirv_hash() const; + private: /** - * @brief Parse the serialization options for the serializer. + * @brief Parse the configuration options for the serializer. * * @param config The JSON configuration. * @@ -104,6 +123,15 @@ class LayerConfig */ void parse_serialization_options(const json& config); + /** + * @brief Parse the configuration options for the shader module. + * + * @param config The JSON configuration. + * + * @throws json::out_of_bounds if required fields are missing. + */ + void parse_shader_options(const json& config); + private: /** * @brief True if we force serialize across queues. @@ -149,4 +177,19 @@ class LayerConfig * @brief True if we force serialize after transfer workloads. */ bool serialize_transfer_post { false }; + + /** + * @brief True if we force disable executable binary caching. + */ + bool shader_disable_program_cache { false }; + + /** + * @brief True if we force remove use of relaxed precision decoration. + */ + bool shader_disable_program_relaxed_precision { false }; + + /** + * @brief True if we change SPIR-V to change the program hash. + */ + bool shader_enable_program_fuzz_spirv_hash { false }; }; diff --git a/layer_gpu_support/source/layer_device_functions.hpp b/layer_gpu_support/source/layer_device_functions.hpp index b6d0302..e13a396 100644 --- a/layer_gpu_support/source/layer_device_functions.hpp +++ b/layer_gpu_support/source/layer_device_functions.hpp @@ -293,3 +293,61 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_vkQueueSubmit2KHR( uint32_t submitCount, const VkSubmitInfo2* pSubmits, VkFence fence); + +// Functions for pipelines + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateShaderModule( + VkDevice device, + const VkShaderModuleCreateInfo* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkShaderModule* pShaderModule); + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateShadersEXT( + VkDevice device, + uint32_t createInfoCount, + const VkShaderCreateInfoEXT* pCreateInfos, + const VkAllocationCallbacks* pAllocator, + VkShaderEXT* pShaders); + + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkGetPipelineKeyKHR( + VkDevice device, + const VkPipelineCreateInfoKHR* pPipelineCreateInfo, + VkPipelineBinaryKeyKHR* pPipelineKey); + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateComputePipelines( + VkDevice device, + VkPipelineCache pipelineCache, + uint32_t createInfoCount, + const VkComputePipelineCreateInfo* pCreateInfos, + const VkAllocationCallbacks* pAllocator, + VkPipeline* pPipelines); + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateGraphicsPipelines( + VkDevice device, + VkPipelineCache pipelineCache, + uint32_t createInfoCount, + const VkGraphicsPipelineCreateInfo* pCreateInfos, + const VkAllocationCallbacks* pAllocator, + VkPipeline* pPipelines); + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateRayTracingPipelinesKHR( + VkDevice device, + VkDeferredOperationKHR deferredOperation, + VkPipelineCache pipelineCache, + uint32_t createInfoCount, + const VkRayTracingPipelineCreateInfoKHR* pCreateInfos, + const VkAllocationCallbacks* pAllocator, + VkPipeline* pPipelines); diff --git a/layer_gpu_support/source/layer_device_functions_pipelines.cpp b/layer_gpu_support/source/layer_device_functions_pipelines.cpp new file mode 100644 index 0000000..6001dd3 --- /dev/null +++ b/layer_gpu_support/source/layer_device_functions_pipelines.cpp @@ -0,0 +1,373 @@ +/* + * SPDX-License-Identifier: MIT + * ---------------------------------------------------------------------------- + * Copyright (c) 2025 Arm Limited + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * ---------------------------------------------------------------------------- + */ + +#include +#include + +#include "device.hpp" +#include "layer_device_functions.hpp" + +extern std::mutex g_vulkanLock; + +// Opcode IDs for interesting SPIR-V opcodes +static const uint32_t SPIRV_OPCODE_OPDECORATE { 71 }; +static const uint32_t SPIRV_OPCODE_OPMEMBERDECORATE { 72 }; +static const uint32_t SPIRV_OPCODE_OPDECORATEID { 332 }; +static const uint32_t SPIRV_OPCODE_OPDECORATESTRING { 5632 }; +static const uint32_t SPIRV_OPCODE_OPMEMBERDECORATESTRING { 5633 }; + +// Map of decorate opcodes and the word offset of the decoration ID +static const std::map SPIRV_DECORATE_OPCODES { + {SPIRV_OPCODE_OPDECORATE, 2}, + {SPIRV_OPCODE_OPMEMBERDECORATE, 3}, + {SPIRV_OPCODE_OPDECORATEID, 2}, + {SPIRV_OPCODE_OPDECORATESTRING, 2}, + {SPIRV_OPCODE_OPMEMBERDECORATESTRING, 3} +}; + +// Decoration IDs for interesting SPIR-V decorations +static const uint32_t SPIRV_DECORATE_RELAXED_PRECISION { 0 }; + +/** + * @brief Modify a SPIR-V module to remove all use of relaxed precision. + * + * @param originalCode The original binary. + * + * @return The modified binary. + */ +static std::vector remove_relaxed_precision( + const std::vector& originalCode +) { + // This module assumes the input SPIR-V is valid + std::vector code; + + // Process SPIR-V header + for (size_t i = 0 ; i < 5; i++) + { + code.push_back(originalCode[i]); + + } + + // Process SPIR-V opcode payload + for (size_t i = 5 ; i < originalCode.size(); /* Increment in loop. */) + { + uint32_t opWord0 = originalCode[i]; + + uint32_t opCode { opWord0 & 0xFFFF }; + uint32_t opWordCount { opWord0 >> 16 }; + + // This should never happen, but avoids infinite loop on bad input + if (opWordCount == 0) + { + LAYER_ERR("SPIR-V opcode has zero length"); + break; + } + + bool keep { true }; + if (isInMap(opCode, SPIRV_DECORATE_OPCODES)) + { + size_t offset = SPIRV_DECORATE_OPCODES.at(opCode); + uint32_t decoration = originalCode[i + offset]; + if (decoration == SPIRV_DECORATE_RELAXED_PRECISION) + { + keep = false; + } + } + + if (keep) + { + for (size_t opI = 0; opI < opWordCount; opI++) + { + code.push_back(originalCode[i + opI]); + } + } + + i += opWordCount; + } + + // Return the modified program + return code; +} + +/** + * @brief Modify a SPIR-V module so it functionally the same with a different hash. + * + * @param originalCode The original binary. + * + * @return The modified binary. + */ +static std::vector fuzz_spirv( + const std::vector& originalCode +) { + // This module assumes the input SPIR-V is valid + std::vector code = originalCode; + + // Opcode NOP = 1 word, Opcode ID 0 + uint32_t opNOP = (1 << 16) | 0; + code.push_back(opNOP); + + return code; +} + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateShaderModule( + VkDevice device, + const VkShaderModuleCreateInfo* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkShaderModule* pShaderModule +) { + LAYER_TRACE(__func__); + + // Hold the lock to access layer-wide global store + std::unique_lock lock { g_vulkanLock }; + auto* layer = Device::retrieve(device); + + // Release the lock to call into the driver + lock.unlock(); + + // Create a copy of the code that we can modify + std::vector code; + code.assign(pCreateInfo->pCode, pCreateInfo->pCode + (pCreateInfo->codeSize / sizeof(uint32_t))); + + if (layer->instance->config.shader_disable_relaxed_precision()) + { + code = remove_relaxed_precision(code); + } + + if (layer->instance->config.shader_fuzz_spirv_hash()) + { + code = fuzz_spirv(code); + } + + VkShaderModuleCreateInfo newCreateInfo = *pCreateInfo; + newCreateInfo.pCode = code.data(); + newCreateInfo.codeSize = code.size() * sizeof(uint32_t); + + return layer->driver.vkCreateShaderModule(device, &newCreateInfo, pAllocator, pShaderModule); +} + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateShadersEXT( + VkDevice device, + uint32_t createInfoCount, + const VkShaderCreateInfoEXT* pCreateInfos, + const VkAllocationCallbacks* pAllocator, + VkShaderEXT* pShaders +) { + LAYER_TRACE(__func__); + + // Hold the lock to access layer-wide global store + std::unique_lock lock { g_vulkanLock }; + auto* layer = Device::retrieve(device); + + // Create copies we can modify + std::vector newCreateInfo; + newCreateInfo.resize(createInfoCount); + + std::vector> newCodes; + for (uint32_t i = 0; i < createInfoCount; i++) + { + newCreateInfo[i] = pCreateInfos[i]; + + // No preprocessing on binary shaders + if (newCreateInfo[i].codeType != VK_SHADER_CODE_TYPE_SPIRV_EXT) + { + continue; + } + + // Preprocess SPIR-V shaders + const uint32_t* pCode = static_cast(newCreateInfo[i].pCode); + uint32_t codeWords = newCreateInfo[i].codeSize / sizeof(uint32_t); + + std::vector newCode; + newCode.assign(pCode, pCode + codeWords); + + if (layer->instance->config.shader_disable_relaxed_precision()) + { + newCode = remove_relaxed_precision(newCode); + } + + if (layer->instance->config.shader_fuzz_spirv_hash()) + { + newCode = fuzz_spirv(newCode); + } + + // Patch the descriptors + newCodes.push_back(std::move(newCode)); + newCreateInfo[i].pCode = newCodes[newCodes.size() - 1].data(); + newCreateInfo[i].codeSize = newCodes[newCodes.size() - 1].size() * sizeof(uint32_t); + } + + // Release the lock to call into the driver + lock.unlock(); + auto result = layer->driver.vkCreateShadersEXT(device, createInfoCount, pCreateInfos, pAllocator, pShaders); + + // On error just return + if ((result != VK_SUCCESS) && (result != VK_INCOMPATIBLE_SHADER_BINARY_EXT)) + { + return result; + } + + // If no need to fiddle just return + if (!layer->instance->config.shader_disable_cache()) + { + return result; + } + + // If caching is disabled reject binaries that the driver didn't reject + for (uint32_t i = 0; i < createInfoCount; i++) + { + // Shader is already invalid + if (pShaders[i] == VK_NULL_HANDLE) + { + continue; + } + + // Shader is not a binary shader + if (pCreateInfos[i].codeType != VK_SHADER_CODE_TYPE_BINARY_EXT) + { + continue; + } + + // Shader is a binary shader so delete it and tell app it failed + layer->driver.vkDestroyShaderEXT(device, pShaders[i], pAllocator); + pShaders[i] = VK_NULL_HANDLE; + result = VK_INCOMPATIBLE_SHADER_BINARY_EXT; + } + + return result; +} + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkGetPipelineKeyKHR( + VkDevice device, + const VkPipelineCreateInfoKHR* pPipelineCreateInfo, + VkPipelineBinaryKeyKHR* pPipelineKey +) { + LAYER_TRACE(__func__); + + // Hold the lock to access layer-wide global store + std::unique_lock lock { g_vulkanLock }; + auto* layer = Device::retrieve(device); + + // If caching is disabled then return a scrambled global key + if (layer->instance->config.shader_disable_cache() && !pPipelineCreateInfo) + { + + for (size_t i = 0; i < VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR; i++) + { + pPipelineKey->key[i] = static_cast(i); + } + + pPipelineKey->keySize = VK_MAX_PIPELINE_BINARY_KEY_SIZE_KHR; + return VK_SUCCESS; + } + + // Release the lock to call into the driver + lock.unlock(); + return layer->driver.vkGetPipelineKeyKHR(device, pPipelineCreateInfo, pPipelineKey); +} + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateComputePipelines( + VkDevice device, + VkPipelineCache pipelineCache, + uint32_t createInfoCount, + const VkComputePipelineCreateInfo* pCreateInfos, + const VkAllocationCallbacks* pAllocator, + VkPipeline* pPipelines +) { + LAYER_TRACE(__func__); + + // Hold the lock to access layer-wide global store + std::unique_lock lock { g_vulkanLock }; + auto* layer = Device::retrieve(device); + + if (layer->instance->config.shader_disable_cache()) + { + pipelineCache = VK_NULL_HANDLE; + } + + // Release the lock to call into the driver + lock.unlock(); + return layer->driver.vkCreateComputePipelines(device, pipelineCache, createInfoCount, pCreateInfos, pAllocator, pPipelines); +} + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateGraphicsPipelines( + VkDevice device, + VkPipelineCache pipelineCache, + uint32_t createInfoCount, + const VkGraphicsPipelineCreateInfo* pCreateInfos, + const VkAllocationCallbacks* pAllocator, + VkPipeline* pPipelines +) { + LAYER_TRACE(__func__); + + // Hold the lock to access layer-wide global store + std::unique_lock lock { g_vulkanLock }; + auto* layer = Device::retrieve(device); + + if (layer->instance->config.shader_disable_cache()) + { + pipelineCache = VK_NULL_HANDLE; + } + + // Release the lock to call into the driver + lock.unlock(); + return layer->driver.vkCreateGraphicsPipelines(device, pipelineCache, createInfoCount, pCreateInfos, pAllocator, pPipelines); +} + +/* See Vulkan API for documentation. */ +template<> +VKAPI_ATTR VkResult VKAPI_CALL layer_vkCreateRayTracingPipelinesKHR( + VkDevice device, + VkDeferredOperationKHR deferredOperation, + VkPipelineCache pipelineCache, + uint32_t createInfoCount, + const VkRayTracingPipelineCreateInfoKHR* pCreateInfos, + const VkAllocationCallbacks* pAllocator, + VkPipeline* pPipelines +) { + LAYER_TRACE(__func__); + + // Hold the lock to access layer-wide global store + std::unique_lock lock { g_vulkanLock }; + auto* layer = Device::retrieve(device); + + if (layer->instance->config.shader_disable_cache()) + { + pipelineCache = VK_NULL_HANDLE; + } + + // Release the lock to call into the driver + lock.unlock(); + return layer->driver.vkCreateRayTracingPipelinesKHR(device, deferredOperation, pipelineCache, createInfoCount, pCreateInfos, pAllocator, pPipelines); +}