Add Vulkan GLSL shader compilation path

This commit is contained in:
2026-03-27 19:30:28 +08:00
parent bf79bd344e
commit 5a49812ea9
8 changed files with 908 additions and 115 deletions

View File

@@ -157,6 +157,7 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanTexture.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanSampler.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanShader.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanShaderCompiler.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanDescriptorPool.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanDescriptorSet.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanPipelineLayout.h
@@ -174,6 +175,7 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanTexture.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanSampler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanShader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanShaderCompiler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanDescriptorPool.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanDescriptorSet.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanPipelineLayout.cpp

View File

@@ -8,12 +8,22 @@
#include "XCEngine/RHI/RHIEnums.h"
#include <cstring>
#include <cmath>
#include <string>
namespace XCEngine {
namespace RHI {
inline std::string NarrowAscii(const std::wstring& value) {
std::string result;
result.reserve(value.size());
for (wchar_t ch : value) {
result.push_back(static_cast<char>(ch));
}
return result;
}
inline std::wstring WidenAscii(const char* value) {
if (value == nullptr) {
return {};
@@ -23,6 +33,42 @@ inline std::wstring WidenAscii(const char* value) {
return std::wstring(ascii.begin(), ascii.end());
}
inline bool TryResolveShaderTypeFromTarget(const char* target, ShaderType& type) {
if (target == nullptr) {
return false;
}
if (std::strstr(target, "vs_") != nullptr || std::strcmp(target, "vs") == 0) {
type = ShaderType::Vertex;
return true;
}
if (std::strstr(target, "ps_") != nullptr ||
std::strstr(target, "fs_") != nullptr ||
std::strcmp(target, "ps") == 0 ||
std::strcmp(target, "fs") == 0) {
type = ShaderType::Fragment;
return true;
}
if (std::strstr(target, "gs_") != nullptr || std::strcmp(target, "gs") == 0) {
type = ShaderType::Geometry;
return true;
}
if (std::strstr(target, "hs_") != nullptr || std::strcmp(target, "hs") == 0) {
type = ShaderType::TessControl;
return true;
}
if (std::strstr(target, "ds_") != nullptr || std::strcmp(target, "ds") == 0) {
type = ShaderType::TessEvaluation;
return true;
}
if (std::strstr(target, "cs_") != nullptr || std::strcmp(target, "cs") == 0) {
type = ShaderType::Compute;
return true;
}
return false;
}
inline VkFormat ToVulkanFormat(Format format) {
switch (format) {
case Format::R8G8B8A8_UNorm:

View File

@@ -0,0 +1,23 @@
#pragma once
#include "XCEngine/RHI/RHITypes.h"
#include <cstdint>
#include <string>
#include <vector>
namespace XCEngine {
namespace RHI {
struct VulkanCompiledShader {
std::vector<uint32_t> spirvWords;
ShaderType type = ShaderType::Vertex;
std::string entryPoint;
};
bool CompileVulkanShader(const ShaderCompileDesc& desc,
VulkanCompiledShader& outShader,
std::string* errorMessage = nullptr);
} // namespace RHI
} // namespace XCEngine

View File

@@ -13,6 +13,7 @@
#include "XCEngine/RHI/Vulkan/VulkanResourceView.h"
#include "XCEngine/RHI/Vulkan/VulkanSampler.h"
#include "XCEngine/RHI/Vulkan/VulkanShader.h"
#include "XCEngine/RHI/Vulkan/VulkanShaderCompiler.h"
#include "XCEngine/RHI/Vulkan/VulkanSwapChain.h"
#include "XCEngine/RHI/Vulkan/VulkanTexture.h"
@@ -27,15 +28,6 @@ namespace RHI {
namespace {
std::string NarrowAscii(const std::wstring& value) {
std::string result;
result.reserve(value.size());
for (wchar_t ch : value) {
result.push_back(static_cast<char>(ch));
}
return result;
}
std::wstring ResolveVendorName(uint32_t vendorId) {
switch (vendorId) {
case 0x10DE: return L"NVIDIA";
@@ -645,27 +637,13 @@ RHICommandQueue* VulkanDevice::CreateCommandQueue(const CommandQueueDesc& desc)
RHIShader* VulkanDevice::CreateShader(const ShaderCompileDesc& desc) {
auto* shader = new VulkanShader(m_device);
bool success = false;
const std::string entryPoint = NarrowAscii(desc.entryPoint);
const std::string profile = NarrowAscii(desc.profile);
const bool isSpirvSource = desc.sourceLanguage == ShaderLanguage::SPIRV;
const bool isSpirvFile = !desc.fileName.empty() &&
std::filesystem::path(desc.fileName).extension() == L".spv";
if (!desc.source.empty() && isSpirvSource) {
success = shader->Compile(
desc.source.data(),
desc.source.size(),
entryPoint.empty() ? nullptr : entryPoint.c_str(),
profile.empty() ? nullptr : profile.c_str());
} else if (!desc.fileName.empty() && (isSpirvSource || isSpirvFile)) {
success = shader->CompileFromFile(
desc.fileName.c_str(),
entryPoint.empty() ? nullptr : entryPoint.c_str(),
profile.empty() ? nullptr : profile.c_str());
}
VulkanCompiledShader compiledShader = {};
const bool success = CompileVulkanShader(desc, compiledShader, nullptr) &&
shader->Compile(
compiledShader.spirvWords.data(),
compiledShader.spirvWords.size() * sizeof(uint32_t),
compiledShader.entryPoint.empty() ? nullptr : compiledShader.entryPoint.c_str(),
nullptr);
if (success) {
return shader;

View File

@@ -3,11 +3,9 @@
#include "XCEngine/RHI/Vulkan/VulkanDevice.h"
#include "XCEngine/RHI/Vulkan/VulkanPipelineLayout.h"
#include "XCEngine/RHI/Vulkan/VulkanShader.h"
#include "XCEngine/RHI/Vulkan/VulkanShaderCompiler.h"
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <vector>
@@ -16,57 +14,6 @@ namespace RHI {
namespace {
std::string NarrowAscii(const std::wstring& value) {
std::string result;
result.reserve(value.size());
for (wchar_t ch : value) {
result.push_back(static_cast<char>(ch));
}
return result;
}
bool LoadSpirvBytes(const ShaderCompileDesc& desc, std::vector<uint32_t>& words, std::string& entryPoint) {
entryPoint = NarrowAscii(desc.entryPoint);
if (entryPoint.empty()) {
entryPoint = "main";
}
if (desc.sourceLanguage != ShaderLanguage::SPIRV) {
return false;
}
std::vector<char> bytes;
if (!desc.source.empty()) {
bytes.assign(desc.source.begin(), desc.source.end());
} else if (!desc.fileName.empty()) {
std::ifstream file(std::filesystem::path(desc.fileName), std::ios::binary | std::ios::ate);
if (!file.is_open()) {
return false;
}
const std::streamsize fileSize = file.tellg();
if (fileSize <= 0 || (fileSize % 4) != 0) {
return false;
}
bytes.resize(static_cast<size_t>(fileSize));
file.seekg(0, std::ios::beg);
if (!file.read(bytes.data(), fileSize)) {
return false;
}
} else {
return false;
}
if ((bytes.size() % sizeof(uint32_t)) != 0) {
return false;
}
words.resize(bytes.size() / sizeof(uint32_t));
std::memcpy(words.data(), bytes.data(), bytes.size());
return !words.empty();
}
bool HasShaderPayload(const ShaderCompileDesc& desc) {
return !desc.source.empty() || !desc.fileName.empty();
}
@@ -154,17 +101,17 @@ bool VulkanPipelineState::EnsurePipelineLayout(const GraphicsPipelineDesc& desc)
}
bool VulkanPipelineState::CreateGraphicsPipeline(const GraphicsPipelineDesc& desc) {
std::vector<uint32_t> vertexWords;
std::vector<uint32_t> fragmentWords;
std::string vertexEntryPoint;
std::string fragmentEntryPoint;
if (!LoadSpirvBytes(desc.vertexShader, vertexWords, vertexEntryPoint) ||
!LoadSpirvBytes(desc.fragmentShader, fragmentWords, fragmentEntryPoint)) {
VulkanCompiledShader vertexShader = {};
VulkanCompiledShader fragmentShader = {};
if (!CompileVulkanShader(desc.vertexShader, vertexShader, nullptr) ||
!CompileVulkanShader(desc.fragmentShader, fragmentShader, nullptr) ||
vertexShader.type != ShaderType::Vertex ||
fragmentShader.type != ShaderType::Fragment) {
return false;
}
const VkShaderModule vertexModule = CreateShaderModule(m_device, vertexWords);
const VkShaderModule fragmentModule = CreateShaderModule(m_device, fragmentWords);
const VkShaderModule vertexModule = CreateShaderModule(m_device, vertexShader.spirvWords);
const VkShaderModule fragmentModule = CreateShaderModule(m_device, fragmentShader.spirvWords);
if (vertexModule == VK_NULL_HANDLE || fragmentModule == VK_NULL_HANDLE) {
if (vertexModule != VK_NULL_HANDLE) {
vkDestroyShaderModule(m_device, vertexModule, nullptr);
@@ -264,12 +211,12 @@ bool VulkanPipelineState::CreateGraphicsPipeline(const GraphicsPipelineDesc& des
shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
shaderStages[0].module = vertexModule;
shaderStages[0].pName = vertexEntryPoint.c_str();
shaderStages[0].pName = vertexShader.entryPoint.c_str();
shaderStages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
shaderStages[1].module = fragmentModule;
shaderStages[1].pName = fragmentEntryPoint.c_str();
shaderStages[1].pName = fragmentShader.entryPoint.c_str();
VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;

View File

@@ -136,27 +136,7 @@ bool VulkanShader::InitializeFromSpirvWords(const uint32_t* words, size_t wordCo
}
bool VulkanShader::ResolveShaderTypeFromTarget(const char* target, ShaderType& type) {
if (target == nullptr) {
return false;
}
if (std::strstr(target, "vs_") != nullptr || std::strcmp(target, "vs") == 0) {
type = ShaderType::Vertex;
return true;
}
if (std::strstr(target, "ps_") != nullptr || std::strstr(target, "fs_") != nullptr || std::strcmp(target, "ps") == 0) {
type = ShaderType::Fragment;
return true;
}
if (std::strstr(target, "gs_") != nullptr || std::strcmp(target, "gs") == 0) {
type = ShaderType::Geometry;
return true;
}
if (std::strstr(target, "cs_") != nullptr || std::strcmp(target, "cs") == 0) {
type = ShaderType::Compute;
return true;
}
return false;
return TryResolveShaderTypeFromTarget(target, type);
}
bool VulkanShader::ResolveShaderTypeFromSpirv(const uint32_t* words, size_t wordCount, ShaderType& type, std::string& entryPoint) {

View File

@@ -0,0 +1,613 @@
#include "XCEngine/RHI/Vulkan/VulkanShaderCompiler.h"
#include "XCEngine/RHI/Vulkan/VulkanCommon.h"
#include <windows.h>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
#include <vector>
namespace XCEngine {
namespace RHI {
namespace {
constexpr uint32_t kSpirvMagic = 0x07230203u;
constexpr uint16_t kSpirvOpEntryPoint = 15;
ShaderType ToShaderType(uint32_t executionModel) {
switch (executionModel) {
case 0:
return ShaderType::Vertex;
case 1:
return ShaderType::TessControl;
case 2:
return ShaderType::TessEvaluation;
case 3:
return ShaderType::Geometry;
case 4:
return ShaderType::Fragment;
case 5:
return ShaderType::Compute;
default:
return ShaderType::Vertex;
}
}
bool LoadBinaryFile(const std::filesystem::path& path, std::vector<uint8_t>& bytes) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
return false;
}
const std::streamsize size = file.tellg();
if (size <= 0) {
return false;
}
bytes.resize(static_cast<size_t>(size));
file.seekg(0, std::ios::beg);
if (!file.read(reinterpret_cast<char*>(bytes.data()), size)) {
return false;
}
return true;
}
bool LoadTextFile(const std::filesystem::path& path, std::string& text) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
return false;
}
const std::streamsize size = file.tellg();
if (size <= 0) {
return false;
}
text.resize(static_cast<size_t>(size));
file.seekg(0, std::ios::beg);
if (!file.read(text.data(), size)) {
return false;
}
return true;
}
bool WriteTextFile(const std::filesystem::path& path, const std::string& text) {
std::ofstream file(path, std::ios::binary | std::ios::trunc);
if (!file.is_open()) {
return false;
}
file.write(text.data(), static_cast<std::streamsize>(text.size()));
return file.good();
}
bool ParseSpirvMetadata(const uint32_t* words,
size_t wordCount,
ShaderType& type,
std::string& entryPoint) {
if (words == nullptr || wordCount < 5 || words[0] != kSpirvMagic) {
return false;
}
size_t index = 5;
while (index < wordCount) {
const uint32_t instruction = words[index];
const uint16_t wordCountInInstruction = static_cast<uint16_t>(instruction >> 16);
const uint16_t opcode = static_cast<uint16_t>(instruction & 0xFFFFu);
if (wordCountInInstruction == 0 || index + wordCountInInstruction > wordCount) {
return false;
}
if (opcode == kSpirvOpEntryPoint && wordCountInInstruction >= 4) {
type = ToShaderType(words[index + 1]);
entryPoint = reinterpret_cast<const char*>(&words[index + 3]);
return true;
}
index += wordCountInInstruction;
}
return false;
}
bool TryResolveShaderTypeFromPath(const std::filesystem::path& path, ShaderType& type) {
std::string extension = path.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
if (extension == ".vert" || extension == ".vs") {
type = ShaderType::Vertex;
return true;
}
if (extension == ".frag" || extension == ".ps" || extension == ".fs") {
type = ShaderType::Fragment;
return true;
}
if (extension == ".geom" || extension == ".gs") {
type = ShaderType::Geometry;
return true;
}
if (extension == ".tesc" || extension == ".hs") {
type = ShaderType::TessControl;
return true;
}
if (extension == ".tese" || extension == ".ds") {
type = ShaderType::TessEvaluation;
return true;
}
if (extension == ".comp" || extension == ".cs") {
type = ShaderType::Compute;
return true;
}
return false;
}
bool TryResolveShaderTypeFromSource(std::string_view source, ShaderType& type) {
std::string lowered(source.begin(), source.end());
std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
if (lowered.find("local_size_x") != std::string::npos ||
lowered.find("gl_globalinvocationid") != std::string::npos ||
lowered.find("imagestore") != std::string::npos ||
lowered.find("imageload") != std::string::npos) {
type = ShaderType::Compute;
return true;
}
if (lowered.find("gl_position") != std::string::npos) {
type = ShaderType::Vertex;
return true;
}
if (lowered.find("gl_fragcoord") != std::string::npos ||
lowered.find("gl_fragdepth") != std::string::npos ||
lowered.find("fragcolor") != std::string::npos) {
type = ShaderType::Fragment;
return true;
}
return false;
}
bool ResolveShaderType(const ShaderCompileDesc& desc,
std::string_view sourceText,
ShaderType& type) {
const std::string profile = NarrowAscii(desc.profile);
if (TryResolveShaderTypeFromTarget(profile.c_str(), type)) {
return true;
}
if (!desc.fileName.empty() && TryResolveShaderTypeFromPath(std::filesystem::path(desc.fileName), type)) {
return true;
}
return TryResolveShaderTypeFromSource(sourceText, type);
}
bool IsSpirvInput(const ShaderCompileDesc& desc) {
return desc.sourceLanguage == ShaderLanguage::SPIRV ||
(!desc.fileName.empty() && std::filesystem::path(desc.fileName).extension() == L".spv");
}
bool IsHlslInput(const ShaderCompileDesc& desc) {
if (desc.sourceLanguage == ShaderLanguage::HLSL) {
return true;
}
if (desc.fileName.empty()) {
return false;
}
std::string extension = std::filesystem::path(desc.fileName).extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return extension == ".hlsl";
}
std::wstring GetEnvironmentVariableValue(const wchar_t* name) {
const DWORD length = GetEnvironmentVariableW(name, nullptr, 0);
if (length == 0) {
return {};
}
std::wstring value(static_cast<size_t>(length), L'\0');
const DWORD written = GetEnvironmentVariableW(name, value.data(), length);
if (written == 0) {
return {};
}
value.resize(written);
return value;
}
bool FindGlslangValidator(std::wstring& outPath) {
const std::wstring vulkanSdk = GetEnvironmentVariableValue(L"VULKAN_SDK");
if (!vulkanSdk.empty()) {
const std::filesystem::path sdkCandidate = std::filesystem::path(vulkanSdk) / L"Bin" / L"glslangValidator.exe";
if (std::filesystem::exists(sdkCandidate)) {
outPath = sdkCandidate.wstring();
return true;
}
}
std::vector<std::filesystem::path> candidates;
const std::filesystem::path sdkRoot = L"D:/VulkanSDK";
if (std::filesystem::exists(sdkRoot) && std::filesystem::is_directory(sdkRoot)) {
for (const auto& entry : std::filesystem::directory_iterator(sdkRoot)) {
if (!entry.is_directory()) {
continue;
}
const std::filesystem::path candidate = entry.path() / L"Bin" / L"glslangValidator.exe";
if (std::filesystem::exists(candidate)) {
candidates.push_back(candidate);
}
}
}
if (!candidates.empty()) {
std::sort(candidates.begin(), candidates.end(), [](const std::filesystem::path& lhs, const std::filesystem::path& rhs) {
return lhs.wstring() > rhs.wstring();
});
outPath = candidates.front().wstring();
return true;
}
wchar_t buffer[MAX_PATH] = {};
const DWORD length = SearchPathW(nullptr, L"glslangValidator.exe", nullptr, MAX_PATH, buffer, nullptr);
if (length > 0 && length < MAX_PATH) {
outPath = buffer;
return true;
}
return false;
}
std::wstring ShaderStageArgument(ShaderType type) {
switch (type) {
case ShaderType::Vertex:
return L"vert";
case ShaderType::Fragment:
return L"frag";
case ShaderType::Geometry:
return L"geom";
case ShaderType::TessControl:
return L"tesc";
case ShaderType::TessEvaluation:
return L"tese";
case ShaderType::Compute:
return L"comp";
default:
return L"vert";
}
}
std::wstring ShaderStageExtension(ShaderType type) {
return std::wstring(L".") + ShaderStageArgument(type);
}
bool CreateTemporaryPath(const wchar_t* extension, std::wstring& outPath) {
wchar_t tempDirectory[MAX_PATH] = {};
if (GetTempPathW(MAX_PATH, tempDirectory) == 0) {
return false;
}
wchar_t tempFile[MAX_PATH] = {};
if (GetTempFileNameW(tempDirectory, L"XCV", 0, tempFile) == 0) {
return false;
}
DeleteFileW(tempFile);
outPath = tempFile;
outPath += extension;
return true;
}
std::string InjectMacrosIntoSource(const std::string& source, const std::vector<ShaderCompileMacro>& macros) {
if (macros.empty()) {
return source;
}
std::string macroBlock;
for (const ShaderCompileMacro& macro : macros) {
macroBlock += "#define ";
macroBlock += NarrowAscii(macro.name);
const std::string definition = NarrowAscii(macro.definition);
if (!definition.empty()) {
macroBlock += ' ';
macroBlock += definition;
}
macroBlock += '\n';
}
if (source.rfind("#version", 0) == 0) {
const size_t lineEnd = source.find('\n');
if (lineEnd != std::string::npos) {
std::string result;
result.reserve(source.size() + macroBlock.size());
result.append(source, 0, lineEnd + 1);
result += macroBlock;
result.append(source, lineEnd + 1, std::string::npos);
return result;
}
}
return macroBlock + source;
}
bool RunProcessAndCapture(const std::wstring& executablePath,
const std::wstring& arguments,
DWORD& exitCode,
std::string& output) {
SECURITY_ATTRIBUTES securityAttributes = {};
securityAttributes.nLength = sizeof(securityAttributes);
securityAttributes.bInheritHandle = TRUE;
HANDLE readPipe = nullptr;
HANDLE writePipe = nullptr;
if (!CreatePipe(&readPipe, &writePipe, &securityAttributes, 0)) {
return false;
}
SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOW startupInfo = {};
startupInfo.cb = sizeof(startupInfo);
startupInfo.dwFlags = STARTF_USESTDHANDLES;
startupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
startupInfo.hStdOutput = writePipe;
startupInfo.hStdError = writePipe;
PROCESS_INFORMATION processInfo = {};
std::wstring commandLine = L"\"" + executablePath + L"\" " + arguments;
std::vector<wchar_t> commandLineBuffer(commandLine.begin(), commandLine.end());
commandLineBuffer.push_back(L'\0');
const BOOL processCreated = CreateProcessW(nullptr,
commandLineBuffer.data(),
nullptr,
nullptr,
TRUE,
CREATE_NO_WINDOW,
nullptr,
nullptr,
&startupInfo,
&processInfo);
CloseHandle(writePipe);
writePipe = nullptr;
if (!processCreated) {
CloseHandle(readPipe);
return false;
}
char buffer[4096] = {};
DWORD bytesRead = 0;
while (ReadFile(readPipe, buffer, sizeof(buffer), &bytesRead, nullptr) && bytesRead > 0) {
output.append(buffer, bytesRead);
}
WaitForSingleObject(processInfo.hProcess, INFINITE);
GetExitCodeProcess(processInfo.hProcess, &exitCode);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
CloseHandle(readPipe);
return true;
}
bool CompileGlslToSpirv(const ShaderCompileDesc& desc,
const std::string& sourceText,
ShaderType type,
const std::string& entryPoint,
std::vector<uint32_t>& spirvWords,
std::string* errorMessage) {
std::wstring validatorPath;
if (!FindGlslangValidator(validatorPath)) {
if (errorMessage != nullptr) {
*errorMessage = "glslangValidator.exe was not found. Install Vulkan SDK or add it to PATH.";
}
return false;
}
std::wstring tempSourcePath;
std::wstring tempOutputPath;
if (!CreateTemporaryPath(ShaderStageExtension(type).c_str(), tempSourcePath) ||
!CreateTemporaryPath(L".spv", tempOutputPath)) {
if (errorMessage != nullptr) {
*errorMessage = "Failed to allocate temporary shader compiler files.";
}
return false;
}
const std::string sourceWithMacros = InjectMacrosIntoSource(sourceText, desc.macros);
const bool wroteSource = WriteTextFile(tempSourcePath, sourceWithMacros);
if (!wroteSource) {
DeleteFileW(tempSourcePath.c_str());
DeleteFileW(tempOutputPath.c_str());
if (errorMessage != nullptr) {
*errorMessage = "Failed to write temporary GLSL shader file.";
}
return false;
}
const std::wstring arguments =
L"-V --target-env vulkan1.0 -S " + ShaderStageArgument(type) +
L" -e " + WidenAscii(entryPoint.c_str()) +
L" -o \"" + tempOutputPath + L"\" \"" + tempSourcePath + L"\"";
DWORD exitCode = 0;
std::string compilerOutput;
const bool ranProcess = RunProcessAndCapture(validatorPath, arguments, exitCode, compilerOutput);
std::vector<uint8_t> bytes;
const bool loadedOutput = ranProcess && exitCode == 0 && LoadBinaryFile(tempOutputPath, bytes);
DeleteFileW(tempSourcePath.c_str());
DeleteFileW(tempOutputPath.c_str());
if (!ranProcess) {
if (errorMessage != nullptr) {
*errorMessage = "Failed to launch glslangValidator.exe.";
}
return false;
}
if (!loadedOutput) {
if (errorMessage != nullptr) {
*errorMessage = compilerOutput.empty()
? "glslangValidator.exe failed to compile GLSL to SPIR-V."
: compilerOutput;
}
return false;
}
if (bytes.empty() || (bytes.size() % sizeof(uint32_t)) != 0) {
if (errorMessage != nullptr) {
*errorMessage = "Compiled SPIR-V payload size is invalid.";
}
return false;
}
spirvWords.resize(bytes.size() / sizeof(uint32_t));
std::memcpy(spirvWords.data(), bytes.data(), bytes.size());
return true;
}
} // namespace
bool CompileVulkanShader(const ShaderCompileDesc& desc,
VulkanCompiledShader& outShader,
std::string* errorMessage) {
outShader = {};
outShader.entryPoint = NarrowAscii(desc.entryPoint);
if (outShader.entryPoint.empty()) {
outShader.entryPoint = "main";
}
if (desc.source.empty() && desc.fileName.empty()) {
if (errorMessage != nullptr) {
*errorMessage = "No shader source or file name was provided.";
}
return false;
}
if (IsSpirvInput(desc)) {
std::vector<uint8_t> bytes;
if (!desc.source.empty()) {
bytes = desc.source;
} else if (!LoadBinaryFile(std::filesystem::path(desc.fileName), bytes)) {
if (errorMessage != nullptr) {
*errorMessage = "Failed to read SPIR-V shader file.";
}
return false;
}
if (bytes.empty() || (bytes.size() % sizeof(uint32_t)) != 0) {
if (errorMessage != nullptr) {
*errorMessage = "SPIR-V payload size is invalid.";
}
return false;
}
outShader.spirvWords.resize(bytes.size() / sizeof(uint32_t));
std::memcpy(outShader.spirvWords.data(), bytes.data(), bytes.size());
if (outShader.spirvWords.empty() || outShader.spirvWords[0] != kSpirvMagic) {
if (errorMessage != nullptr) {
*errorMessage = "SPIR-V payload does not contain a valid SPIR-V header.";
}
return false;
}
std::string parsedEntryPoint;
if (!ParseSpirvMetadata(outShader.spirvWords.data(),
outShader.spirvWords.size(),
outShader.type,
parsedEntryPoint)) {
const std::string profile = NarrowAscii(desc.profile);
if (!TryResolveShaderTypeFromTarget(profile.c_str(), outShader.type)) {
if (errorMessage != nullptr) {
*errorMessage = "Failed to parse SPIR-V shader metadata.";
}
return false;
}
} else if (desc.entryPoint.empty() && !parsedEntryPoint.empty()) {
outShader.entryPoint = parsedEntryPoint;
}
return true;
}
if (IsHlslInput(desc)) {
if (errorMessage != nullptr) {
*errorMessage = "The Vulkan backend currently supports GLSL or SPIR-V inputs, not HLSL source.";
}
return false;
}
std::string sourceText;
if (!desc.source.empty()) {
sourceText.assign(reinterpret_cast<const char*>(desc.source.data()), desc.source.size());
} else if (!LoadTextFile(std::filesystem::path(desc.fileName), sourceText)) {
if (errorMessage != nullptr) {
*errorMessage = "Failed to read GLSL shader file.";
}
return false;
}
if (sourceText.empty()) {
if (errorMessage != nullptr) {
*errorMessage = "GLSL shader source is empty.";
}
return false;
}
if (!ResolveShaderType(desc, sourceText, outShader.type)) {
if (errorMessage != nullptr) {
*errorMessage = "Failed to resolve Vulkan shader stage from profile, file extension, or source text.";
}
return false;
}
if (!CompileGlslToSpirv(desc,
sourceText,
outShader.type,
outShader.entryPoint,
outShader.spirvWords,
errorMessage)) {
return false;
}
ShaderType parsedType = outShader.type;
std::string parsedEntryPoint;
if (ParseSpirvMetadata(outShader.spirvWords.data(),
outShader.spirvWords.size(),
parsedType,
parsedEntryPoint)) {
outShader.type = parsedType;
if (desc.entryPoint.empty() && !parsedEntryPoint.empty()) {
outShader.entryPoint = parsedEntryPoint;
}
}
return true;
}
} // namespace RHI
} // namespace XCEngine

View File

@@ -1,10 +1,13 @@
#if defined(XCENGINE_SUPPORT_VULKAN)
#include <windows.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <vector>
#include "XCEngine/RHI/RHICommandList.h"
@@ -28,6 +31,17 @@ using namespace XCEngine::RHI;
namespace {
std::wstring ResolveShaderPath(const wchar_t* relativePath) {
wchar_t exePath[MAX_PATH] = {};
const DWORD length = GetModuleFileNameW(nullptr, exePath, MAX_PATH);
std::filesystem::path rootPath =
length > 0 ? std::filesystem::path(exePath).parent_path() : std::filesystem::current_path();
for (int i = 0; i < 5; ++i) {
rootPath = rootPath.parent_path();
}
return (rootPath / relativePath).wstring();
}
VkImageLayout ToImageLayout(ResourceStates state) {
switch (state) {
case ResourceStates::RenderTarget:
@@ -206,6 +220,21 @@ protected:
return m_device->CreateShader(shaderDesc);
}
RHIShader* CreateWriteRedComputeShaderFromGlsl() const {
static const char* computeSource = R"(#version 450
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(set = 0, binding = 0, rgba8) uniform writeonly image2D uImage;
void main() {
imageStore(uImage, ivec2(0, 0), vec4(1.0, 0.0, 0.0, 1.0));
}
)";
ShaderCompileDesc shaderDesc = {};
shaderDesc.sourceLanguage = ShaderLanguage::GLSL;
shaderDesc.source.assign(computeSource, computeSource + std::strlen(computeSource));
return m_device->CreateShader(shaderDesc);
}
void SubmitAndWait(RHICommandList* commandList) {
ASSERT_NE(commandList, nullptr);
commandList->Close();
@@ -464,6 +493,97 @@ TEST_F(VulkanGraphicsFixture, CreateShaderFromSpirvProducesValidComputeShader) {
delete shader;
}
TEST_F(VulkanGraphicsFixture, CreateShaderFromGlslSourceProducesValidVertexShader) {
static const char* vertexSource = R"(#version 450
layout(location = 0) in vec4 aPosition;
void main() {
gl_Position = aPosition;
}
)";
ShaderCompileDesc shaderDesc = {};
shaderDesc.sourceLanguage = ShaderLanguage::GLSL;
shaderDesc.profile = L"vs_4_50";
shaderDesc.source.assign(vertexSource, vertexSource + std::strlen(vertexSource));
RHIShader* shader = m_device->CreateShader(shaderDesc);
ASSERT_NE(shader, nullptr);
EXPECT_TRUE(shader->IsValid());
EXPECT_EQ(shader->GetType(), ShaderType::Vertex);
EXPECT_NE(shader->GetNativeHandle(), nullptr);
shader->Shutdown();
delete shader;
}
TEST_F(VulkanGraphicsFixture, CreateShaderFromGlslFileProducesValidVertexShader) {
ShaderCompileDesc shaderDesc = {};
shaderDesc.fileName = ResolveShaderPath(L"tests/RHI/integration/triangle/Res/Shader/triangle_vulkan.vert");
shaderDesc.entryPoint = L"main";
shaderDesc.profile = L"vs_4_50";
RHIShader* shader = m_device->CreateShader(shaderDesc);
ASSERT_NE(shader, nullptr);
EXPECT_TRUE(shader->IsValid());
EXPECT_EQ(shader->GetType(), ShaderType::Vertex);
EXPECT_NE(shader->GetNativeHandle(), nullptr);
shader->Shutdown();
delete shader;
}
TEST_F(VulkanGraphicsFixture, CreateShaderFromGlslSourceInfersComputeShader) {
RHIShader* shader = CreateWriteRedComputeShaderFromGlsl();
ASSERT_NE(shader, nullptr);
EXPECT_TRUE(shader->IsValid());
EXPECT_EQ(shader->GetType(), ShaderType::Compute);
EXPECT_NE(shader->GetNativeHandle(), nullptr);
shader->Shutdown();
delete shader;
}
TEST_F(VulkanGraphicsFixture, CreateGraphicsPipelineFromGlslShadersProducesValidPipeline) {
static const char* vertexSource = R"(#version 450
layout(location = 0) in vec4 aPosition;
void main() {
gl_Position = aPosition;
}
)";
static const char* fragmentSource = R"(#version 450
layout(location = 0) out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
)";
GraphicsPipelineDesc pipelineDesc = {};
pipelineDesc.topologyType = static_cast<uint32_t>(PrimitiveTopologyType::Triangle);
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(Format::Unknown);
InputElementDesc position = {};
position.semanticName = "POSITION";
position.semanticIndex = 0;
position.format = static_cast<uint32_t>(Format::R32G32B32A32_Float);
position.inputSlot = 0;
position.alignedByteOffset = 0;
pipelineDesc.inputLayout.elements.push_back(position);
pipelineDesc.vertexShader.sourceLanguage = ShaderLanguage::GLSL;
pipelineDesc.vertexShader.profile = L"vs_4_50";
pipelineDesc.vertexShader.source.assign(vertexSource, vertexSource + std::strlen(vertexSource));
pipelineDesc.fragmentShader.sourceLanguage = ShaderLanguage::GLSL;
pipelineDesc.fragmentShader.profile = L"fs_4_50";
pipelineDesc.fragmentShader.source.assign(fragmentSource, fragmentSource + std::strlen(fragmentSource));
RHIPipelineState* pipelineState = m_device->CreatePipelineState(pipelineDesc);
ASSERT_NE(pipelineState, nullptr);
EXPECT_TRUE(pipelineState->IsValid());
EXPECT_NE(pipelineState->GetNativeHandle(), nullptr);
pipelineState->Shutdown();
delete pipelineState;
}
TEST_F(VulkanGraphicsFixture, CreateUnorderedAccessViewProducesValidView) {
RHITexture* texture = m_device->CreateTexture(CreateColorTextureDesc(4, 4));
ASSERT_NE(texture, nullptr);
@@ -567,6 +687,90 @@ TEST_F(VulkanGraphicsFixture, DispatchWritesUavTexture) {
delete texture;
}
TEST_F(VulkanGraphicsFixture, DispatchWritesUavTextureWithGlslComputeShader) {
RHITexture* texture = m_device->CreateTexture(CreateColorTextureDesc(4, 4));
ASSERT_NE(texture, nullptr);
ResourceViewDesc uavDesc = {};
uavDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
uavDesc.dimension = ResourceViewDimension::Texture2D;
RHIResourceView* uav = m_device->CreateUnorderedAccessView(texture, uavDesc);
ASSERT_NE(uav, nullptr);
DescriptorPoolDesc poolDesc = {};
poolDesc.type = DescriptorHeapType::CBV_SRV_UAV;
poolDesc.descriptorCount = 1;
poolDesc.shaderVisible = true;
RHIDescriptorPool* pool = m_device->CreateDescriptorPool(poolDesc);
ASSERT_NE(pool, nullptr);
DescriptorSetLayoutBinding uavBinding = {};
uavBinding.binding = 0;
uavBinding.type = static_cast<uint32_t>(DescriptorType::UAV);
uavBinding.count = 1;
uavBinding.visibility = static_cast<uint32_t>(ShaderVisibility::All);
DescriptorSetLayoutDesc setLayout = {};
setLayout.bindings = &uavBinding;
setLayout.bindingCount = 1;
RHIPipelineLayoutDesc pipelineLayoutDesc = {};
pipelineLayoutDesc.setLayouts = &setLayout;
pipelineLayoutDesc.setLayoutCount = 1;
RHIPipelineLayout* pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc);
ASSERT_NE(pipelineLayout, nullptr);
RHIDescriptorSet* descriptorSet = pool->AllocateSet(setLayout);
ASSERT_NE(descriptorSet, nullptr);
descriptorSet->Update(0, uav);
GraphicsPipelineDesc pipelineDesc = {};
pipelineDesc.pipelineLayout = pipelineLayout;
RHIPipelineState* pipelineState = m_device->CreatePipelineState(pipelineDesc);
ASSERT_NE(pipelineState, nullptr);
RHIShader* shader = CreateWriteRedComputeShaderFromGlsl();
ASSERT_NE(shader, nullptr);
pipelineState->SetComputeShader(shader);
EXPECT_TRUE(pipelineState->HasComputeShader());
EXPECT_EQ(pipelineState->GetType(), PipelineType::Compute);
RHICommandList* commandList = CreateCommandList();
ASSERT_NE(commandList, nullptr);
commandList->Reset();
commandList->TransitionBarrier(uav, ResourceStates::Common, ResourceStates::UnorderedAccess);
commandList->SetPipelineState(pipelineState);
RHIDescriptorSet* descriptorSets[] = { descriptorSet };
commandList->SetComputeDescriptorSets(0, 1, descriptorSets, pipelineLayout);
commandList->Dispatch(1, 1, 1);
SubmitAndWait(commandList);
const std::vector<uint8_t> pixels = ReadTextureRgba8(static_cast<VulkanTexture*>(texture));
ASSERT_GE(pixels.size(), 4u);
EXPECT_EQ(pixels[0], 255u);
EXPECT_EQ(pixels[1], 0u);
EXPECT_EQ(pixels[2], 0u);
EXPECT_EQ(pixels[3], 255u);
commandList->Shutdown();
delete commandList;
shader->Shutdown();
delete shader;
pipelineState->Shutdown();
delete pipelineState;
descriptorSet->Shutdown();
delete descriptorSet;
pipelineLayout->Shutdown();
delete pipelineLayout;
pool->Shutdown();
delete pool;
uav->Shutdown();
delete uav;
texture->Shutdown();
delete texture;
}
} // namespace
#endif