diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index c1d7f977..e438191e 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -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 diff --git a/engine/include/XCEngine/RHI/Vulkan/VulkanCommon.h b/engine/include/XCEngine/RHI/Vulkan/VulkanCommon.h index 3b443ba7..95878e90 100644 --- a/engine/include/XCEngine/RHI/Vulkan/VulkanCommon.h +++ b/engine/include/XCEngine/RHI/Vulkan/VulkanCommon.h @@ -8,12 +8,22 @@ #include "XCEngine/RHI/RHIEnums.h" +#include #include #include 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(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: diff --git a/engine/include/XCEngine/RHI/Vulkan/VulkanShaderCompiler.h b/engine/include/XCEngine/RHI/Vulkan/VulkanShaderCompiler.h new file mode 100644 index 00000000..df976366 --- /dev/null +++ b/engine/include/XCEngine/RHI/Vulkan/VulkanShaderCompiler.h @@ -0,0 +1,23 @@ +#pragma once + +#include "XCEngine/RHI/RHITypes.h" + +#include +#include +#include + +namespace XCEngine { +namespace RHI { + +struct VulkanCompiledShader { + std::vector spirvWords; + ShaderType type = ShaderType::Vertex; + std::string entryPoint; +}; + +bool CompileVulkanShader(const ShaderCompileDesc& desc, + VulkanCompiledShader& outShader, + std::string* errorMessage = nullptr); + +} // namespace RHI +} // namespace XCEngine diff --git a/engine/src/RHI/Vulkan/VulkanDevice.cpp b/engine/src/RHI/Vulkan/VulkanDevice.cpp index 2d89bb6b..7f8ce9d5 100644 --- a/engine/src/RHI/Vulkan/VulkanDevice.cpp +++ b/engine/src/RHI/Vulkan/VulkanDevice.cpp @@ -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(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; diff --git a/engine/src/RHI/Vulkan/VulkanPipelineState.cpp b/engine/src/RHI/Vulkan/VulkanPipelineState.cpp index 874085d9..2187f50a 100644 --- a/engine/src/RHI/Vulkan/VulkanPipelineState.cpp +++ b/engine/src/RHI/Vulkan/VulkanPipelineState.cpp @@ -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 -#include -#include -#include #include #include @@ -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(ch)); - } - return result; -} - -bool LoadSpirvBytes(const ShaderCompileDesc& desc, std::vector& words, std::string& entryPoint) { - entryPoint = NarrowAscii(desc.entryPoint); - if (entryPoint.empty()) { - entryPoint = "main"; - } - - if (desc.sourceLanguage != ShaderLanguage::SPIRV) { - return false; - } - - std::vector 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(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 vertexWords; - std::vector 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; diff --git a/engine/src/RHI/Vulkan/VulkanShader.cpp b/engine/src/RHI/Vulkan/VulkanShader.cpp index 6047990c..4c2217cb 100644 --- a/engine/src/RHI/Vulkan/VulkanShader.cpp +++ b/engine/src/RHI/Vulkan/VulkanShader.cpp @@ -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) { diff --git a/engine/src/RHI/Vulkan/VulkanShaderCompiler.cpp b/engine/src/RHI/Vulkan/VulkanShaderCompiler.cpp new file mode 100644 index 00000000..b0c74e4f --- /dev/null +++ b/engine/src/RHI/Vulkan/VulkanShaderCompiler.cpp @@ -0,0 +1,613 @@ +#include "XCEngine/RHI/Vulkan/VulkanShaderCompiler.h" + +#include "XCEngine/RHI/Vulkan/VulkanCommon.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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& 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)); + file.seekg(0, std::ios::beg); + if (!file.read(reinterpret_cast(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)); + 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(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(instruction >> 16); + const uint16_t opcode = static_cast(instruction & 0xFFFFu); + if (wordCountInInstruction == 0 || index + wordCountInInstruction > wordCount) { + return false; + } + + if (opcode == kSpirvOpEntryPoint && wordCountInInstruction >= 4) { + type = ToShaderType(words[index + 1]); + entryPoint = reinterpret_cast(&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(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(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(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(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 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& 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 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& 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 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 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(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 diff --git a/tests/RHI/unit/test_vulkan_graphics.cpp b/tests/RHI/unit/test_vulkan_graphics.cpp index 4db566f7..71d6621c 100644 --- a/tests/RHI/unit/test_vulkan_graphics.cpp +++ b/tests/RHI/unit/test_vulkan_graphics.cpp @@ -1,10 +1,13 @@ #if defined(XCENGINE_SUPPORT_VULKAN) +#include + #include #include #include #include +#include #include #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(PrimitiveTopologyType::Triangle); + pipelineDesc.renderTargetFormats[0] = static_cast(Format::R8G8B8A8_UNorm); + pipelineDesc.depthStencilFormat = static_cast(Format::Unknown); + + InputElementDesc position = {}; + position.semanticName = "POSITION"; + position.semanticIndex = 0; + position.format = static_cast(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(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(DescriptorType::UAV); + uavBinding.count = 1; + uavBinding.visibility = static_cast(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 pixels = ReadTextureRgba8(static_cast(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