/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ShaderProgram.h" #include "graphics/ShaderDefines.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/XML/Xeromyces.h" #include "renderer/backend/vulkan/DescriptorManager.h" #include "renderer/backend/vulkan/Device.h" #include "renderer/backend/vulkan/Texture.h" #include #include namespace Renderer { namespace Backend { namespace Vulkan { namespace { VkShaderModule CreateShaderModule(CDevice* device, const VfsPath& path) { CVFSFile file; if (file.Load(g_VFS, path) != PSRETURN_OK) { LOGERROR("Failed to load shader file: '%s'", path.string8()); return VK_NULL_HANDLE; } VkShaderModuleCreateInfo createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; // Casting to uint32_t requires to fit alignment and size. ENSURE(file.GetBufferSize() % 4 == 0); ENSURE(reinterpret_cast(file.GetBuffer()) % alignof(uint32_t) == 0u); createInfo.codeSize = file.GetBufferSize(); createInfo.pCode = reinterpret_cast(file.GetBuffer()); VkShaderModule shaderModule; if (vkCreateShaderModule(device->GetVkDevice(), &createInfo, nullptr, &shaderModule) != VK_SUCCESS) { LOGERROR("Failed to create shader module from file: '%s'", path.string8()); return VK_NULL_HANDLE; } device->SetObjectName(VK_OBJECT_TYPE_SHADER_MODULE, shaderModule, path.string8().c_str()); return shaderModule; } VfsPath FindProgramMatchingDefines(const VfsPath& xmlFilename, const CShaderDefines& defines) { CXeromyces xeroFile; PSRETURN ret = xeroFile.Load(g_VFS, xmlFilename); if (ret != PSRETURN_OK) return {}; // TODO: add XML validation. #define EL(x) const int el_##x = xeroFile.GetElementID(#x) #define AT(x) const int at_##x = xeroFile.GetAttributeID(#x) EL(define); EL(defines); EL(program); AT(file); AT(name); AT(value); #undef AT #undef EL const CStrIntern strUndefined("UNDEFINED"); VfsPath programFilename; XMBElement root = xeroFile.GetRoot(); XERO_ITER_EL(root, rootChild) { if (rootChild.GetNodeName() == el_program) { CShaderDefines programDefines; XERO_ITER_EL(rootChild, programChild) { if (programChild.GetNodeName() == el_defines) { XERO_ITER_EL(programChild, definesChild) { XMBAttributeList attributes = definesChild.GetAttributes(); if (definesChild.GetNodeName() == el_define) { const CStrIntern value(attributes.GetNamedItem(at_value)); if (value == strUndefined) continue; programDefines.Add( CStrIntern(attributes.GetNamedItem(at_name)), value); } } } } if (programDefines == defines) return L"shaders/" + rootChild.GetAttributes().GetNamedItem(at_file).FromUTF8(); } } return {}; } } // anonymous namespace IDevice* CVertexInputLayout::GetDevice() { return m_Device; } // static std::unique_ptr CShaderProgram::Create( CDevice* device, const CStr& name, const CShaderDefines& baseDefines) { const VfsPath xmlFilename = L"shaders/" + wstring_from_utf8(name) + L".xml"; std::unique_ptr shaderProgram(new CShaderProgram()); shaderProgram->m_Device = device; shaderProgram->m_FileDependencies = {xmlFilename}; CShaderDefines defines = baseDefines; if (device->GetDescriptorManager().UseDescriptorIndexing()) defines.Add(str_USE_DESCRIPTOR_INDEXING, str_1); const VfsPath programFilename = FindProgramMatchingDefines(xmlFilename, defines); if (programFilename.empty()) { LOGERROR("Program '%s' with required defines not found.", name); for (const auto& pair : defines.GetMap()) LOGERROR(" \"%s\": \"%s\"", pair.first.c_str(), pair.second.c_str()); return nullptr; } shaderProgram->m_FileDependencies.emplace_back(programFilename); CXeromyces programXeroFile; if (programXeroFile.Load(g_VFS, programFilename) != PSRETURN_OK) return nullptr; XMBElement programRoot = programXeroFile.GetRoot(); #define EL(x) const int el_##x = programXeroFile.GetElementID(#x) #define AT(x) const int at_##x = programXeroFile.GetAttributeID(#x) EL(binding); EL(descriptor_set); EL(descriptor_sets); EL(fragment); EL(member); EL(push_constant); EL(stream); EL(vertex); AT(binding); AT(file); AT(location); AT(name); AT(offset); AT(set); AT(size); AT(type); #undef AT #undef EL auto addPushConstant = [&pushConstants=shaderProgram->m_PushConstants, &pushConstantDataFlags=shaderProgram->m_PushConstantDataFlags, &at_name, &at_offset, &at_size]( const XMBElement& element, VkShaderStageFlags stageFlags) -> bool { const XMBAttributeList attributes = element.GetAttributes(); const CStrIntern name = CStrIntern(attributes.GetNamedItem(at_name)); const uint32_t size = attributes.GetNamedItem(at_size).ToUInt(); const uint32_t offset = attributes.GetNamedItem(at_offset).ToUInt(); if (offset % 4 != 0 || size % 4 != 0) { LOGERROR("Push constant should have offset and size be multiple of 4."); return false; } for (PushConstant& pushConstant : pushConstants) { if (pushConstant.name == name) { if (size != pushConstant.size || offset != pushConstant.offset) { LOGERROR("All shared push constants must have the same size and offset."); return false; } // We found the same constant so we don't need to add it again. pushConstant.stageFlags |= stageFlags; for (uint32_t index = 0; index < (size >> 2); ++index) pushConstantDataFlags[(offset >> 2) + index] |= stageFlags; return true; } if (offset + size < pushConstant.offset || offset >= pushConstant.offset + pushConstant.size) continue; LOGERROR("All push constant must not intersect each other in memory."); return false; } pushConstants.push_back({name, offset, size, stageFlags}); for (uint32_t index = 0; index < (size >> 2); ++index) pushConstantDataFlags[(offset >> 2) + index] = stageFlags; return true; }; auto addDescriptorSets = [&](const XMBElement& element) -> bool { const bool useDescriptorIndexing = device->GetDescriptorManager().UseDescriptorIndexing(); // TODO: reduce the indentation. XERO_ITER_EL(element, descriporSetsChild) { if (descriporSetsChild.GetNodeName() == el_descriptor_set) { const uint32_t set = descriporSetsChild.GetAttributes().GetNamedItem(at_set).ToUInt(); if (useDescriptorIndexing && set == 0 && !descriporSetsChild.GetChildNodes().empty()) { LOGERROR("Descritor set for descriptor indexing shouldn't contain bindings."); return false; } XERO_ITER_EL(descriporSetsChild, descriporSetChild) { if (descriporSetChild.GetNodeName() == el_binding) { const XMBAttributeList attributes = descriporSetChild.GetAttributes(); const uint32_t binding = attributes.GetNamedItem(at_binding).ToUInt(); const uint32_t size = attributes.GetNamedItem(at_size).ToUInt(); const CStr type = attributes.GetNamedItem(at_type); if (type == "uniform") { const uint32_t expectedSet = device->GetDescriptorManager().GetUniformSet(); if (set != expectedSet || binding != 0) { LOGERROR("We support only a single uniform block per shader program."); return false; } shaderProgram->m_MaterialConstantsDataSize = size; XERO_ITER_EL(descriporSetChild, bindingChild) { if (bindingChild.GetNodeName() == el_member) { const XMBAttributeList memberAttributes = bindingChild.GetAttributes(); const uint32_t offset = memberAttributes.GetNamedItem(at_offset).ToUInt(); const uint32_t size = memberAttributes.GetNamedItem(at_size).ToUInt(); const CStrIntern name{memberAttributes.GetNamedItem(at_name)}; bool found = false; for (const Uniform& uniform : shaderProgram->m_Uniforms) { if (uniform.name == name) { if (offset != uniform.offset || size != uniform.size) { LOGERROR("All uniforms across all stage should match."); return false; } found = true; } else { if (offset + size <= uniform.offset || uniform.offset + uniform.size <= offset) continue; LOGERROR("Uniforms must not overlap each other."); return false; } } if (!found) shaderProgram->m_Uniforms.push_back({name, offset, size}); } } } else if (type == "sampler1D" || type == "sampler2D" || type == "sampler2DShadow" || type == "sampler3D" || type == "samplerCube") { if (useDescriptorIndexing) { LOGERROR("We support only uniform descriptor sets with enabled descriptor indexing."); return false; } const CStrIntern name{attributes.GetNamedItem(at_name)}; shaderProgram->m_TextureMapping[name] = binding; shaderProgram->m_TexturesDescriptorSetSize = std::max(shaderProgram->m_TexturesDescriptorSetSize, binding + 1); } else { LOGERROR("Unsupported binding: '%s'", type.c_str()); return false; } } } } } return true; }; XERO_ITER_EL(programRoot, programChild) { if (programChild.GetNodeName() == el_vertex) { const VfsPath shaderModulePath = L"shaders/" + programChild.GetAttributes().GetNamedItem(at_file).FromUTF8(); shaderProgram->m_FileDependencies.emplace_back(shaderModulePath); shaderProgram->m_ShaderModules.emplace_back( CreateShaderModule(device, shaderModulePath)); if (shaderProgram->m_ShaderModules.back() == VK_NULL_HANDLE) return nullptr; VkPipelineShaderStageCreateInfo vertexShaderStageInfo{}; vertexShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; vertexShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; vertexShaderStageInfo.module = shaderProgram->m_ShaderModules.back(); vertexShaderStageInfo.pName = "main"; shaderProgram->m_Stages.emplace_back(std::move(vertexShaderStageInfo)); XERO_ITER_EL(programChild, stageChild) { if (stageChild.GetNodeName() == el_stream) { XMBAttributeList attributes = stageChild.GetAttributes(); const uint32_t location = attributes.GetNamedItem(at_location).ToUInt(); const CStr streamName = attributes.GetNamedItem(at_name); VertexAttributeStream stream = VertexAttributeStream::UV7; if (streamName == "pos") stream = VertexAttributeStream::POSITION; else if (streamName == "normal") stream = VertexAttributeStream::NORMAL; else if (streamName == "color") stream = VertexAttributeStream::COLOR; else if (streamName == "uv0") stream = VertexAttributeStream::UV0; else if (streamName == "uv1") stream = VertexAttributeStream::UV1; else if (streamName == "uv2") stream = VertexAttributeStream::UV2; else if (streamName == "uv3") stream = VertexAttributeStream::UV3; else if (streamName == "uv4") stream = VertexAttributeStream::UV4; else if (streamName == "uv5") stream = VertexAttributeStream::UV5; else if (streamName == "uv6") stream = VertexAttributeStream::UV6; else if (streamName == "uv7") stream = VertexAttributeStream::UV7; else debug_warn("Unknown stream"); shaderProgram->m_StreamLocations[stream] = location; } else if (stageChild.GetNodeName() == el_push_constant) { if (!addPushConstant(stageChild, VK_SHADER_STAGE_VERTEX_BIT)) return nullptr; } else if (stageChild.GetNodeName() == el_descriptor_sets) { if (!addDescriptorSets(stageChild)) return nullptr; } } } else if (programChild.GetNodeName() == el_fragment) { const VfsPath shaderModulePath = L"shaders/" + programChild.GetAttributes().GetNamedItem(at_file).FromUTF8(); shaderProgram->m_FileDependencies.emplace_back(shaderModulePath); shaderProgram->m_ShaderModules.emplace_back( CreateShaderModule(device, shaderModulePath)); if (shaderProgram->m_ShaderModules.back() == VK_NULL_HANDLE) return nullptr; VkPipelineShaderStageCreateInfo fragmentShaderStageInfo{}; fragmentShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; fragmentShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; fragmentShaderStageInfo.module = shaderProgram->m_ShaderModules.back(); fragmentShaderStageInfo.pName = "main"; shaderProgram->m_Stages.emplace_back(std::move(fragmentShaderStageInfo)); XERO_ITER_EL(programChild, stageChild) { if (stageChild.GetNodeName() == el_push_constant) { if (!addPushConstant(stageChild, VK_SHADER_STAGE_FRAGMENT_BIT)) return nullptr; } else if (stageChild.GetNodeName() == el_descriptor_sets) { if (!addDescriptorSets(stageChild)) return nullptr; } } } } if (shaderProgram->m_Stages.empty()) { LOGERROR("Program should contain at least one stage."); return nullptr; } for (size_t index = 0; index < shaderProgram->m_PushConstants.size(); ++index) shaderProgram->m_PushConstantMapping[shaderProgram->m_PushConstants[index].name] = index; std::vector pushConstantRanges; pushConstantRanges.reserve(shaderProgram->m_PushConstants.size()); std::transform( shaderProgram->m_PushConstants.begin(), shaderProgram->m_PushConstants.end(), std::back_insert_iterator(pushConstantRanges), [](const PushConstant& pushConstant) { return VkPushConstantRange{pushConstant.stageFlags, pushConstant.offset, pushConstant.size}; }); if (!pushConstantRanges.empty()) { std::sort(pushConstantRanges.begin(), pushConstantRanges.end(), [](const VkPushConstantRange& lhs, const VkPushConstantRange& rhs) { return lhs.offset < rhs.offset; }); // Merge subsequent constants. auto it = pushConstantRanges.begin(); while (std::next(it) != pushConstantRanges.end()) { auto next = std::next(it); if (it->stageFlags == next->stageFlags) { it->size = next->offset - it->offset + next->size; pushConstantRanges.erase(next); } else it = next; } for (const VkPushConstantRange& range : pushConstantRanges) if (std::count_if(pushConstantRanges.begin(), pushConstantRanges.end(), [stageFlags=range.stageFlags](const VkPushConstantRange& range) { return range.stageFlags & stageFlags; }) != 1) { LOGERROR("Any two range must not include the same stage in stageFlags."); return nullptr; } } for (size_t index = 0; index < shaderProgram->m_Uniforms.size(); ++index) shaderProgram->m_UniformMapping[shaderProgram->m_Uniforms[index].name] = index; if (!shaderProgram->m_Uniforms.empty()) { if (shaderProgram->m_MaterialConstantsDataSize > device->GetChoosenPhysicalDevice().properties.limits.maxUniformBufferRange) { LOGERROR("Uniform buffer size is too big for the device."); return nullptr; } shaderProgram->m_MaterialConstantsData = std::make_unique(shaderProgram->m_MaterialConstantsDataSize); } std::vector layouts = device->GetDescriptorManager().GetDescriptorSetLayouts(); if (shaderProgram->m_TexturesDescriptorSetSize > 0) { ENSURE(!device->GetDescriptorManager().UseDescriptorIndexing()); shaderProgram->m_BoundTextures.resize(shaderProgram->m_TexturesDescriptorSetSize); shaderProgram->m_BoundTexturesUID.resize(shaderProgram->m_TexturesDescriptorSetSize); shaderProgram->m_BoundTexturesOutdated = true; shaderProgram->m_TexturesDescriptorSetLayout = device->GetDescriptorManager().GetSingleTypeDescritorSetLayout( VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, shaderProgram->m_TexturesDescriptorSetSize); layouts.emplace_back(shaderProgram->m_TexturesDescriptorSetLayout); } VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo{}; pipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipelineLayoutCreateInfo.setLayoutCount = layouts.size(); pipelineLayoutCreateInfo.pSetLayouts = layouts.data(); pipelineLayoutCreateInfo.pushConstantRangeCount = pushConstantRanges.size(); pipelineLayoutCreateInfo.pPushConstantRanges = pushConstantRanges.data(); const VkResult result = vkCreatePipelineLayout( device->GetVkDevice(), &pipelineLayoutCreateInfo, nullptr, &shaderProgram->m_PipelineLayout); if (result != VK_SUCCESS) { LOGERROR("Failed to create a pipeline layout: %d", static_cast(result)); return nullptr; } return shaderProgram; } CShaderProgram::CShaderProgram() = default; CShaderProgram::~CShaderProgram() { if (m_PipelineLayout != VK_NULL_HANDLE) m_Device->ScheduleObjectToDestroy(VK_OBJECT_TYPE_PIPELINE_LAYOUT, m_PipelineLayout, VK_NULL_HANDLE); for (VkShaderModule shaderModule : m_ShaderModules) if (shaderModule != VK_NULL_HANDLE) m_Device->ScheduleObjectToDestroy(VK_OBJECT_TYPE_SHADER_MODULE, shaderModule, VK_NULL_HANDLE); } IDevice* CShaderProgram::GetDevice() { return m_Device; } int32_t CShaderProgram::GetBindingSlot(const CStrIntern name) const { if (auto it = m_PushConstantMapping.find(name); it != m_PushConstantMapping.end()) return it->second; if (auto it = m_UniformMapping.find(name); it != m_UniformMapping.end()) return it->second + m_PushConstants.size(); if (auto it = m_TextureMapping.find(name); it != m_TextureMapping.end()) return it->second + m_PushConstants.size() + m_UniformMapping.size(); return -1; } std::vector CShaderProgram::GetFileDependencies() const { return m_FileDependencies; } uint32_t CShaderProgram::GetStreamLocation(const VertexAttributeStream stream) const { auto it = m_StreamLocations.find(stream); return it != m_StreamLocations.end() ? it->second : std::numeric_limits::max(); } void CShaderProgram::Bind() { if (m_MaterialConstantsData) m_MaterialConstantsDataOutdated = true; } void CShaderProgram::Unbind() { if (m_TexturesDescriptorSetSize > 0) { for (CTexture*& texture : m_BoundTextures) texture = nullptr; for (CTexture::UID& uid : m_BoundTexturesUID) uid = 0; m_BoundTexturesOutdated = true; } } void CShaderProgram::PreDraw(VkCommandBuffer commandBuffer) { UpdateActiveDescriptorSet(commandBuffer); if (m_PushConstantDataMask) { for (uint32_t index = 0; index < 32;) { if (!(m_PushConstantDataMask & (1 << index))) { ++index; continue; } uint32_t indexEnd = index + 1; while (indexEnd < 32 && (m_PushConstantDataMask & (1 << indexEnd)) && m_PushConstantDataFlags[index] == m_PushConstantDataFlags[indexEnd]) ++indexEnd; vkCmdPushConstants( commandBuffer, GetPipelineLayout(), m_PushConstantDataFlags[index], index * 4, (indexEnd - index) * 4, m_PushConstantData.data() + index * 4); index = indexEnd; } m_PushConstantDataMask = 0; } } void CShaderProgram::UpdateActiveDescriptorSet( VkCommandBuffer commandBuffer) { if (m_BoundTexturesOutdated) { m_BoundTexturesOutdated = false; m_ActiveTexturesDescriptorSet = m_Device->GetDescriptorManager().GetSingleTypeDescritorSet( VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, m_TexturesDescriptorSetLayout, m_BoundTexturesUID, m_BoundTextures); ENSURE(m_ActiveTexturesDescriptorSet != VK_NULL_HANDLE); vkCmdBindDescriptorSets( commandBuffer, GetPipelineBindPoint(), GetPipelineLayout(), 1, 1, &m_ActiveTexturesDescriptorSet, 0, nullptr); } } void CShaderProgram::SetUniform( const int32_t bindingSlot, const float value) { const float values[1] = {value}; SetUniform(bindingSlot, PS::span(values, values + 1)); } void CShaderProgram::SetUniform( const int32_t bindingSlot, const float valueX, const float valueY) { const float values[2] = {valueX, valueY}; SetUniform(bindingSlot, PS::span(values, values + 2)); } void CShaderProgram::SetUniform( const int32_t bindingSlot, const float valueX, const float valueY, const float valueZ) { const float values[3] = {valueX, valueY, valueZ}; SetUniform(bindingSlot, PS::span(values, values + 3)); } void CShaderProgram::SetUniform( const int32_t bindingSlot, const float valueX, const float valueY, const float valueZ, const float valueW) { const float values[4] = {valueX, valueY, valueZ, valueW}; SetUniform(bindingSlot, PS::span(values, values + 4)); } void CShaderProgram::SetUniform(const int32_t bindingSlot, PS::span values) { if (bindingSlot < 0) return; const auto data = GetUniformData(bindingSlot, values.size() * sizeof(float)); std::memcpy(data.first, values.data(), data.second); } std::pair CShaderProgram::GetUniformData( const int32_t bindingSlot, const uint32_t dataSize) { if (bindingSlot < static_cast(m_PushConstants.size())) { const uint32_t size = m_PushConstants[bindingSlot].size; const uint32_t offset = m_PushConstants[bindingSlot].offset; ENSURE(size <= dataSize); m_PushConstantDataMask |= ((1 << (size >> 2)) - 1) << (offset >> 2); return {m_PushConstantData.data() + offset, size}; } else { ENSURE(bindingSlot - m_PushConstants.size() < m_Uniforms.size()); const Uniform& uniform = m_Uniforms[bindingSlot - m_PushConstants.size()]; m_MaterialConstantsDataOutdated = true; const uint32_t size = uniform.size; const uint32_t offset = uniform.offset; ENSURE(size <= dataSize); return {m_MaterialConstantsData.get() + offset, size}; } } void CShaderProgram::SetTexture(const int32_t bindingSlot, CTexture* texture) { if (bindingSlot < 0) return; CDescriptorManager& descriptorManager = m_Device->GetDescriptorManager(); if (descriptorManager.UseDescriptorIndexing()) { const uint32_t descriptorIndex = descriptorManager.GetTextureDescriptor(texture->As()); ENSURE(bindingSlot < static_cast(m_PushConstants.size())); const uint32_t size = m_PushConstants[bindingSlot].size; const uint32_t offset = m_PushConstants[bindingSlot].offset; ENSURE(size == sizeof(descriptorIndex)); std::memcpy(m_PushConstantData.data() + offset, &descriptorIndex, size); m_PushConstantDataMask |= ((1 << (size >> 2)) - 1) << (offset >> 2); } else { ENSURE(bindingSlot >= static_cast(m_PushConstants.size() + m_UniformMapping.size())); const uint32_t index = bindingSlot - (m_PushConstants.size() + m_UniformMapping.size()); if (m_BoundTexturesUID[index] != texture->GetUID()) { m_BoundTextures[index] = texture; m_BoundTexturesUID[index] = texture->GetUID(); m_BoundTexturesOutdated = true; } } } } // namespace Vulkan } // namespace Backend } // namespace Renderer