vk: spec const workaround through runtime spirv modification (#7399)

To avoid driver edge cases with spec const, we manually change the
spec const into a regular constant in the spirv.

FIXES=310603393
This commit is contained in:
Powei Feng
2023-12-07 14:49:12 -08:00
committed by GitHub
parent 5a9c47bcb5
commit 56bd2871ec
10 changed files with 200 additions and 66 deletions
+2
View File
@@ -743,6 +743,8 @@ endif()
if (FILAMENT_SUPPORTS_VULKAN)
add_subdirectory(${LIBRARIES}/bluevk)
add_subdirectory(${EXTERNAL}/vkmemalloc/tnt)
set(SPIRV_HEADERS_SKIP_EXAMPLES ON)
add_subdirectory(${EXTERNAL}/spirv-headers)
endif()
set(FILAMENT_SAMPLES_BINARY_DIR ${PROJECT_BINARY_DIR}/samples)
+3
View File
@@ -169,6 +169,8 @@ if (FILAMENT_SUPPORTS_VULKAN)
src/vulkan/platform/VulkanPlatform.cpp
src/vulkan/platform/VulkanPlatformSwapChainImpl.cpp
src/vulkan/platform/VulkanPlatformSwapChainImpl.h
src/vulkan/spirv/VulkanSpirvUtils.cpp
src/vulkan/spirv/VulkanSpirvUtils.h
src/vulkan/VulkanBlitter.cpp
src/vulkan/VulkanBlitter.h
src/vulkan/VulkanBuffer.cpp
@@ -313,6 +315,7 @@ endif()
if (FILAMENT_SUPPORTS_VULKAN)
target_link_libraries(${TARGET} PUBLIC bluevk vkmemalloc vkshaders smol-v)
target_link_libraries(${TARGET} PRIVATE SPIRV-Headers)
endif()
if (FILAMENT_SUPPORTS_METAL)
@@ -1194,8 +1194,8 @@ using FrameScheduledCallback = void(*)(PresentCallable callable, void* user);
enum class Workaround : uint16_t {
// The EASU pass must split because shader compiler flattens early-exit branch
SPLIT_EASU,
// Backend allows feedback loop with ancillary buffers (depth/stencil) as long as they're read-only for
// the whole render pass.
// Backend allows feedback loop with ancillary buffers (depth/stencil) as long as they're
// read-only for the whole render pass.
ALLOW_READ_ONLY_ANCILLARY_FEEDBACK_LOOP,
// for some uniform arrays, it's needed to do an initialization to avoid crash on adreno gpu
ADRENO_UNIFORM_ARRAY_CRASH,
+4 -2
View File
@@ -54,7 +54,8 @@ VmaAllocator createAllocator(VkInstance instance, VkPhysicalDevice physicalDevic
VmaAllocator allocator;
VmaVulkanFunctions const funcs {
#if VMA_DYNAMIC_VULKAN_FUNCTIONS
.vkGetInstanceProcAddr = vkGetInstanceProcAddr, .vkGetDeviceProcAddr = vkGetDeviceProcAddr,
.vkGetInstanceProcAddr = vkGetInstanceProcAddr,
.vkGetDeviceProcAddr = vkGetDeviceProcAddr,
#else
.vkGetPhysicalDeviceProperties = vkGetPhysicalDeviceProperties,
.vkGetPhysicalDeviceMemoryProperties = vkGetPhysicalDeviceMemoryProperties,
@@ -426,7 +427,8 @@ void VulkanDriver::destroyTexture(Handle<HwTexture> th) {
}
void VulkanDriver::createProgramR(Handle<HwProgram> ph, Program&& program) {
auto vkprogram = mResourceAllocator.construct<VulkanProgram>(ph, mPlatform->getDevice(), program);
auto vkprogram
= mResourceAllocator.construct<VulkanProgram>(ph, mPlatform->getDevice(), program);
mResourceManager.acquire(vkprogram);
}
+16 -42
View File
@@ -18,6 +18,7 @@
#include "VulkanConstants.h"
#include "VulkanMemory.h"
#include "spirv/VulkanSpirvUtils.h"
#include <backend/platforms/VulkanPlatform.h>
@@ -46,61 +47,34 @@ static void clampToFramebuffer(VkRect2D* rect, uint32_t fbWidth, uint32_t fbHeig
rect->extent.height = std::max(top - y, 0);
}
VulkanProgram::VulkanProgram(VkDevice device, const Program& builder) noexcept
VulkanProgram::VulkanProgram(VkDevice device, Program& builder) noexcept
: HwProgram(builder.getName()),
VulkanResource(VulkanResourceType::PROGRAM),
mInfo(new PipelineInfo(builder.getSpecializationConstants().size())),
mInfo(new PipelineInfo()),
mDevice(device) {
auto& blobs = builder.getShadersSource();
Program::ShaderSource& blobs = builder.getShadersSource();
auto& modules = mInfo->shaders;
auto const& specializationConstants = builder.getSpecializationConstants();
for (size_t i = 0; i < MAX_SHADER_MODULES; i++) {
const auto& blob = blobs[i];
uint32_t* data = (uint32_t*)blob.data();
Program::ShaderBlob& blob = blobs[i];
workaroundSpecConstant(blob, specializationConstants);
uint32_t* data = (uint32_t*) blob.data();
size_t dataSize = blob.size();
VkShaderModule& module = modules[i];
VkShaderModuleCreateInfo moduleInfo = {
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = blob.size(),
.codeSize = dataSize,
.pCode = data,
};
VkResult result = vkCreateShaderModule(mDevice, &moduleInfo, VKALLOC, &module);
ASSERT_POSTCONDITION(result == VK_SUCCESS, "Unable to create shader module.");
}
// Note that bools are 4-bytes in Vulkan
// https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkBool32.html
constexpr uint32_t const CONSTANT_SIZE = 4;
// populate the specialization constants requirements right now
auto const& specializationConstants = builder.getSpecializationConstants();
uint32_t const specConstCount = static_cast<uint32_t>(specializationConstants.size());
char* specData = mInfo->specConstData.get();
if (specConstCount > 0) {
mInfo->specializationInfo = {
.mapEntryCount = specConstCount,
.pMapEntries = mInfo->specConsts.data(),
.dataSize = specConstCount * CONSTANT_SIZE,
.pData = specData,
};
}
for (uint32_t i = 0; i < specConstCount; ++i) {
uint32_t const offset = i * CONSTANT_SIZE;
mInfo->specConsts[i] = {
.constantID = specializationConstants[i].id,
.offset = offset,
.size = CONSTANT_SIZE,
};
using SpecConstant = Program::SpecializationConstant::Type;
char const* addr = (char*)specData + offset;
SpecConstant const& arg = specializationConstants[i].value;
if (std::holds_alternative<bool>(arg)) {
*((VkBool32*)addr) = std::get<bool>(arg) ? VK_TRUE : VK_FALSE;
} else if (std::holds_alternative<float>(arg)) {
*((float*)addr) = std::get<float>(arg);
} else {
*((int32_t*)addr) = std::get<int32_t>(arg);
}
}
auto& groupInfo = builder.getSamplerGroupInfo();
auto& bindingToSamplerIndex = mInfo->bindingToSamplerIndex;
auto& usage = mInfo->usage;
@@ -123,7 +97,7 @@ VulkanProgram::VulkanProgram(VkDevice device, const Program& builder) noexcept
VulkanProgram::VulkanProgram(VkDevice device, VkShaderModule vs, VkShaderModule fs,
CustomSamplerInfoList const& samplerInfo) noexcept
: VulkanResource(VulkanResourceType::PROGRAM),
mInfo(new PipelineInfo(0)),
mInfo(new PipelineInfo()),
mDevice(device) {
mInfo->shaders[0] = vs;
mInfo->shaders[1] = fs;
+5 -16
View File
@@ -25,7 +25,6 @@
#include "VulkanResources.h"
#include "VulkanSwapChain.h"
#include "VulkanTexture.h"
#include "VulkanUtility.h"
#include "private/backend/SamplerGroup.h"
@@ -38,7 +37,7 @@ class VulkanTimestamps;
struct VulkanProgram : public HwProgram, VulkanResource {
VulkanProgram(VkDevice device, const Program& builder) noexcept;
VulkanProgram(VkDevice device, Program& builder) noexcept;
struct CustomSamplerInfo {
uint8_t groupIndex;
@@ -65,21 +64,14 @@ struct VulkanProgram : public HwProgram, VulkanResource {
return mInfo->bindingToSamplerIndex;
}
inline VkSpecializationInfo const& getSpecConstInfo() const {
return mInfo->specializationInfo;
}
private:
// TODO: handle compute shaders.
// The expected order of shaders - from frontend to backend - is vertex, fragment, compute.
static constexpr uint8_t MAX_SHADER_MODULES = 2;
static constexpr uint8_t const MAX_SHADER_MODULES = 2;
struct PipelineInfo {
PipelineInfo(size_t specConstsCount) :
bindingToSamplerIndex(MAX_SAMPLER_COUNT, 0xffff),
specConsts(specConstsCount, VkSpecializationMapEntry{}),
specConstData(new char[specConstsCount * 4])
{}
PipelineInfo()
: bindingToSamplerIndex(MAX_SAMPLER_COUNT, 0xffff) {}
// This bitset maps to each of the sampler in the sampler groups associated with this
// program, and whether each sampler is used in which shader (i.e. vert, frag, compute).
@@ -87,10 +79,7 @@ private:
// We store the samplerGroupIndex as the top 8-bit and the index within each group as the lower 8-bit.
utils::FixedCapacityVector<uint16_t> bindingToSamplerIndex;
VkShaderModule shaders[MAX_SHADER_MODULES] = {VK_NULL_HANDLE};
VkSpecializationInfo specializationInfo = {};
utils::FixedCapacityVector<VkSpecializationMapEntry> specConsts;
std::unique_ptr<char[]> specConstData;
VkShaderModule shaders[MAX_SHADER_MODULES] = { VK_NULL_HANDLE };
};
PipelineInfo* mInfo;
@@ -360,9 +360,7 @@ VulkanPipelineCache::PipelineCacheEntry* VulkanPipelineCache::createPipeline() n
// If we reach this point, we need to create and stash a brand new pipeline object.
shaderStages[0].module = mPipelineRequirements.shaders[0];
shaderStages[0].pSpecializationInfo = mSpecializationRequirements;
shaderStages[1].module = mPipelineRequirements.shaders[1];
shaderStages[1].pSpecializationInfo = mSpecializationRequirements;
// Expand our size-optimized structs into the proper Vk structs.
uint32_t numVertexAttribs = 0;
@@ -568,7 +566,6 @@ VulkanPipelineCache::PipelineLayoutCacheEntry* VulkanPipelineCache::getOrCreateP
void VulkanPipelineCache::bindProgram(VulkanProgram* program) noexcept {
mPipelineRequirements.shaders[0] = program->getVertexShader();
mPipelineRequirements.shaders[1] = program->getFragmentShader();
mSpecializationRequirements = &program->getSpecConstInfo();
}
void VulkanPipelineCache::bindRasterState(const RasterState& rasterState) noexcept {
@@ -416,7 +416,6 @@ private:
RasterState mCurrentRasterState;
PipelineKey mPipelineRequirements = {};
DescriptorKey mDescriptorRequirements = {};
VkSpecializationInfo const* mSpecializationRequirements = nullptr;
// Current bindings for the pipeline and descriptor sets.
PipelineKey mBoundPipeline = {};
@@ -0,0 +1,125 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "VulkanSpirvUtils.h"
#include <utils/Log.h>
#include <bluevk/BlueVK.h>
#include <spirv/unified1/spirv.hpp>
#include <unordered_map>
namespace filament::backend {
using namespace bluevk;
namespace {
// This function transforms an OpSpecConstant instruction into just a OpConstant instruction.
// Additionally, it will adjust the value of the constant as given by the program.
void getTransformedConstantInst(SpecConstantValue const& value, uint32_t* inst) {
// The first word is the size of the instruction and the instruction type.
// The second word is the type of the instruction.
// The third word is the "result id" (i.e. the variable to store into).
// The fourth word (only for floats and ints) is the literal representing the value.
// We only want to change the first and fourth words.
constexpr size_t const OP = 0;
constexpr size_t const VALUE = 3;
if (std::holds_alternative<bool>(value)) {
inst[OP] = (3 << 16)
| (std::get<bool>(value) ? spv::Op::OpConstantTrue : spv::Op::OpConstantFalse);
} else if (std::holds_alternative<float>(value)) {
float const fval = std::get<float>(value);
inst[OP] = (4 << 16) | spv::Op::OpConstant;
inst[VALUE] = *reinterpret_cast<uint32_t const*>(&fval);
} else {
int const ival = std::get<int>(value);
inst[OP] = (4 << 16) | spv::Op::OpConstant;
inst[VALUE] = *reinterpret_cast<uint32_t const*>(&ival);
}
}
} // anonymous namespace
void workaroundSpecConstant(Program::ShaderBlob& blob,
utils::FixedCapacityVector<Program::SpecializationConstant> const& specConstants) {
using WordMap = std::unordered_map<uint32_t, uint32_t>;
using SpecValueMap = std::unordered_map<uint32_t, SpecConstantValue>;
WordMap varToIdMap;
SpecValueMap idToValue;
for (auto spec: specConstants) {
idToValue[spec.id] = spec.value;
}
// The follow loop will iterate through all the instructions and look for instructions of the
// form:
//
// OpDecorate %1 SpecId 0
// %1 = OpSpecConstantFalse %bool
//
// We want to track the mapping between the variable id %1 and the specId 0. And when seeing a
// %1 as the result, we want to change that instruction to just an OpConsant where the constant
// value is adjusted by the spec values provided in the program.
// Additionally, we will turn the SpecId decorations to no-ops.
uint32_t* outputData = (uint32_t*) blob.data();
for (uint32_t cursor = 5, cursorEnd = blob.size() / 4; cursor < cursorEnd;) {
uint32_t const firstWord = outputData[cursor];
uint32_t const wordCount = firstWord >> 16;
uint32_t const op = firstWord & 0x0000FFFF;
switch(op) {
case spv::Op::OpDecorate: {
if (outputData[cursor + 2] == spv::Decoration::DecorationSpecId) {
uint32_t const targetVar = outputData[cursor + 1];
uint32_t const specId = outputData[cursor + 3];
varToIdMap[targetVar] = specId;
// Now we write noops over the decoration since its no longer needed.
for (size_t i = cursor, n = cursor + wordCount; i < n; ++i) {
outputData[i] = (1 << 16) | spv::Op::OpNop;
}
}
break;
}
case spv::Op::OpSpecConstant:
case spv::Op::OpSpecConstantTrue:
case spv::Op::OpSpecConstantFalse: {
uint32_t const targetVar = outputData[cursor + 2];
UTILS_UNUSED_IN_RELEASE WordMap::const_iterator idItr = varToIdMap.find(targetVar);
assert_invariant(idItr != varToIdMap.end() &&
"Cannot find variable previously decorated with SpecId");
assert_invariant(idToValue.find(idItr->second) != idToValue.end() &&
"Spec constant value not provided");
auto const& val = idToValue[varToIdMap[targetVar]];
getTransformedConstantInst(val, &outputData[cursor]);
break;
}
default:
break;
}
cursor += wordCount;
}
}
} // namespace filament::backend
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_FILAMENT_BACKEND_VULKANSPIRVUTILS_H
#define TNT_FILAMENT_BACKEND_VULKANSPIRVUTILS_H
#include "backend/Program.h"
#include <utils/FixedCapacityVector.h>
#include <vector>
namespace filament::backend {
using SpecConstantValue = Program::SpecializationConstant::Type;
// For certain drivers, using spec constant can lead to compile errors [1] or undesirable behaviors
// [2]. In those instances, we simply change the spirv and set them to constants. This function will
// modify the blob (but it's safe to do so since blobs are "moved" to the backend).
//
// [1]: QC driver cannot use spec constant to size fields
// (https://github.com/google/filament/issues/6444).
// [2]: An internal driver does not DCE a block guarded by a spec-const boolean set to false
// (b/310603393).
void workaroundSpecConstant(Program::ShaderBlob& blob,
utils::FixedCapacityVector<Program::SpecializationConstant> const& specConstants);
} // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_VULKANSPIRVUTILS_H