From 97e986b52c42af31299c6e63a5ed74016d7a95fe Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Mon, 6 Apr 2026 18:07:13 +0800 Subject: [PATCH] rendering: add opengl hlsl shader translation --- engine/CMakeLists.txt | 2 + .../XCEngine/RHI/OpenGL/OpenGLShader.h | 3 +- .../RHI/ShaderCompiler/SpirvShaderCompiler.h | 33 + .../RHI/Vulkan/VulkanShaderCompiler.h | 10 +- engine/src/RHI/OpenGL/OpenGLDevice.cpp | 481 ++++++++- engine/src/RHI/OpenGL/OpenGLShader.cpp | 16 + .../ShaderCompiler/SpirvShaderCompiler.cpp | 936 ++++++++++++++++++ .../src/RHI/Vulkan/VulkanShaderCompiler.cpp | 711 +------------ .../RHI/OpenGL/unit/test_backend_specific.cpp | 121 +++ 9 files changed, 1561 insertions(+), 752 deletions(-) create mode 100644 engine/include/XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h create mode 100644 engine/src/RHI/ShaderCompiler/SpirvShaderCompiler.cpp diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 3738c3f7..4d02b455 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -187,6 +187,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/RHIScreenshot.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/RHIDescriptorPool.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/RHIDescriptorSet.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/D3D12/D3D12Enums.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/D3D12/D3D12Device.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/D3D12/D3D12CommandQueue.h @@ -249,6 +250,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanSwapChain.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanDevice.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/RHI/Vulkan/VulkanScreenshot.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/ShaderCompiler/SpirvShaderCompiler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanBuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanTexture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/Vulkan/VulkanSampler.cpp diff --git a/engine/include/XCEngine/RHI/OpenGL/OpenGLShader.h b/engine/include/XCEngine/RHI/OpenGL/OpenGLShader.h index 7a83b30d..222521aa 100644 --- a/engine/include/XCEngine/RHI/OpenGL/OpenGLShader.h +++ b/engine/include/XCEngine/RHI/OpenGL/OpenGLShader.h @@ -23,6 +23,7 @@ public: bool Compile(const char* vertexSource, const char* fragmentSource, const char* geometrySource); bool CompileCompute(const char* computeSource); bool Compile(const char* source, ShaderType type); + bool AdoptProgram(unsigned int program, ShaderType type); void Use() const; @@ -49,4 +50,4 @@ private: }; } // namespace RHI -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/engine/include/XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h b/engine/include/XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h new file mode 100644 index 00000000..92122a04 --- /dev/null +++ b/engine/include/XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h @@ -0,0 +1,33 @@ +#pragma once + +#include "XCEngine/RHI/RHITypes.h" + +#include +#include +#include + +namespace XCEngine { +namespace RHI { + +enum class SpirvTargetEnvironment : uint8_t { + Vulkan = 0, + OpenGL = 1 +}; + +struct CompiledSpirvShader { + std::vector spirvWords; + ShaderType type = ShaderType::Vertex; + std::string entryPoint; +}; + +bool CompileSpirvShader(const ShaderCompileDesc& desc, + SpirvTargetEnvironment targetEnvironment, + CompiledSpirvShader& outShader, + std::string* errorMessage = nullptr); + +bool TranspileSpirvToOpenGLGLSL(const CompiledSpirvShader& shader, + std::string& outSourceText, + std::string* errorMessage = nullptr); + +} // namespace RHI +} // namespace XCEngine diff --git a/engine/include/XCEngine/RHI/Vulkan/VulkanShaderCompiler.h b/engine/include/XCEngine/RHI/Vulkan/VulkanShaderCompiler.h index df976366..0a4d264f 100644 --- a/engine/include/XCEngine/RHI/Vulkan/VulkanShaderCompiler.h +++ b/engine/include/XCEngine/RHI/Vulkan/VulkanShaderCompiler.h @@ -1,19 +1,13 @@ #pragma once -#include "XCEngine/RHI/RHITypes.h" +#include "XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h" -#include #include -#include namespace XCEngine { namespace RHI { -struct VulkanCompiledShader { - std::vector spirvWords; - ShaderType type = ShaderType::Vertex; - std::string entryPoint; -}; +using VulkanCompiledShader = CompiledSpirvShader; bool CompileVulkanShader(const ShaderCompileDesc& desc, VulkanCompiledShader& outShader, diff --git a/engine/src/RHI/OpenGL/OpenGLDevice.cpp b/engine/src/RHI/OpenGL/OpenGLDevice.cpp index 4065df39..2effa80d 100644 --- a/engine/src/RHI/OpenGL/OpenGLDevice.cpp +++ b/engine/src/RHI/OpenGL/OpenGLDevice.cpp @@ -23,9 +23,14 @@ #include "XCEngine/RHI/OpenGL/OpenGLDescriptorPool.h" #include "XCEngine/RHI/OpenGL/OpenGLDescriptorSet.h" #include "XCEngine/RHI/OpenGL/OpenGLPipelineLayout.h" +#include "XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h" #include "XCEngine/Debug/Logger.h" #include #include +#include +#include +#include +#include typedef const char* (WINAPI* PFNWGLGETEXTENSIONSSTRINGARBPROC)(HDC hdc); typedef BOOL (WINAPI* PFNWGLCHOOSEPIXELFORMATARBPROC)(HDC hdc, const int* piAttribIList, const FLOAT* pfAttribFList, UINT nMaxFormats, int* piFormats, UINT* nNumFormats); @@ -135,6 +140,406 @@ std::string SourceToString(const ShaderCompileDesc& desc) { return std::string(reinterpret_cast(desc.source.data()), desc.source.size()); } +bool LoadShaderSourceText(const ShaderCompileDesc& desc, std::string& outSourceText) { + outSourceText.clear(); + if (!desc.source.empty()) { + outSourceText.assign(reinterpret_cast(desc.source.data()), desc.source.size()); + return true; + } + + if (desc.fileName.empty()) { + return false; + } + + std::ifstream file(std::filesystem::path(desc.fileName), std::ios::binary | std::ios::ate); + if (!file.is_open()) { + return false; + } + + const std::streamsize size = file.tellg(); + if (size <= 0) { + return false; + } + + outSourceText.resize(static_cast(size)); + file.seekg(0, std::ios::beg); + return file.read(outSourceText.data(), size).good(); +} + +bool TryResolveShaderTypeFromTarget(const std::wstring& profile, ShaderType& type) { + const std::string asciiProfile = NarrowAscii(profile); + if (asciiProfile.find("vs_") != std::string::npos || asciiProfile == "vs") { + type = ShaderType::Vertex; + return true; + } + if (asciiProfile.find("ps_") != std::string::npos || + asciiProfile.find("fs_") != std::string::npos || + asciiProfile == "ps" || + asciiProfile == "fs") { + type = ShaderType::Fragment; + return true; + } + if (asciiProfile.find("gs_") != std::string::npos || asciiProfile == "gs") { + type = ShaderType::Geometry; + return true; + } + if (asciiProfile.find("hs_") != std::string::npos || asciiProfile == "hs") { + type = ShaderType::TessControl; + return true; + } + if (asciiProfile.find("ds_") != std::string::npos || asciiProfile == "ds") { + type = ShaderType::TessEvaluation; + return true; + } + if (asciiProfile.find("cs_") != std::string::npos || asciiProfile == "cs") { + type = ShaderType::Compute; + return true; + } + return false; +} + +bool TryResolveShaderTypeFromPath(const std::wstring& fileName, ShaderType& type) { + if (fileName.empty()) { + return false; + } + + std::string extension = std::filesystem::path(fileName).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 == ".fs" || extension == ".ps") { + 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 UsesSpirvCompilationPath(const ShaderCompileDesc& desc) { + if (desc.sourceLanguage == ShaderLanguage::HLSL || desc.sourceLanguage == ShaderLanguage::SPIRV) { + 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" || extension == ".spv"; +} + +bool TryResolveShaderTypeFromSource(const std::string& sourceText, ShaderType& type) { + const std::string lowered = ToLowerAscii(sourceText); + if (lowered.find("layout(local_size_x") != std::string::npos || + lowered.find("gl_globalinvocationid") != std::string::npos || + lowered.find("gl_localinvocationid") != std::string::npos || + lowered.find("gl_workgroupid") != std::string::npos) { + type = ShaderType::Compute; + return true; + } + + if (lowered.find("emitvertex") != std::string::npos || + lowered.find("endprimitive") != std::string::npos || + lowered.find("gl_in[") != std::string::npos) { + type = ShaderType::Geometry; + return true; + } + + if (lowered.find("layout(vertices") != std::string::npos || + lowered.find("gl_invocationid") != std::string::npos) { + type = ShaderType::TessControl; + return true; + } + + if (lowered.find("gl_tesscoord") != std::string::npos || + lowered.find("gl_tesslevelouter") != std::string::npos) { + type = ShaderType::TessEvaluation; + 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_fragcolor") != std::string::npos || + lowered.find("gl_fragdata") != std::string::npos || + lowered.find("fragcolor") != std::string::npos) { + type = ShaderType::Fragment; + return true; + } + + return false; +} + +bool ResolveOpenGLShaderType(const ShaderCompileDesc& desc, const std::string& sourceText, ShaderType& type) { + if (TryResolveShaderTypeFromTarget(desc.profile, type)) { + return true; + } + + if (TryResolveShaderTypeFromPath(desc.fileName, type)) { + return true; + } + + return TryResolveShaderTypeFromSource(sourceText, type); +} + +const char* ShaderStageLabel(ShaderType type) { + switch (type) { + case ShaderType::Vertex: return "VERTEX"; + case ShaderType::Fragment: return "FRAGMENT"; + case ShaderType::Geometry: return "GEOMETRY"; + case ShaderType::Compute: return "COMPUTE"; + case ShaderType::TessControl: + case ShaderType::Hull: + return "TESS_CONTROL"; + case ShaderType::TessEvaluation: + case ShaderType::Domain: + return "TESS_EVALUATION"; + default: + return "UNKNOWN"; + } +} + +bool TryConvertToOpenGLShaderType(ShaderType type, GLenum& glShaderType) { + switch (type) { + case ShaderType::Vertex: + glShaderType = GL_VERTEX_SHADER; + return true; + case ShaderType::Fragment: + glShaderType = GL_FRAGMENT_SHADER; + return true; + case ShaderType::Geometry: + glShaderType = GL_GEOMETRY_SHADER; + return true; + case ShaderType::Compute: + glShaderType = GL_COMPUTE_SHADER; + return true; + case ShaderType::TessControl: + case ShaderType::Hull: + glShaderType = GL_TESS_CONTROL_SHADER; + return true; + case ShaderType::TessEvaluation: + case ShaderType::Domain: + glShaderType = GL_TESS_EVALUATION_SHADER; + return true; + default: + return false; + } +} + +bool CheckShaderCompileStatus(GLuint shader, const char* stageLabel, std::string* errorMessage) { + GLint compileStatus = GL_FALSE; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus); + if (compileStatus == GL_TRUE) { + return true; + } + + GLint infoLogLength = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength); + std::vector infoLog(static_cast(std::max(infoLogLength, 1)), '\0'); + glGetShaderInfoLog(shader, infoLogLength, nullptr, infoLog.data()); + if (errorMessage != nullptr) { + *errorMessage = std::string("ERROR::SHADER_COMPILATION_ERROR: ") + stageLabel + "\n" + infoLog.data(); + } + return false; +} + +bool CheckProgramLinkStatus(GLuint program, std::string* errorMessage) { + GLint linkStatus = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &linkStatus); + if (linkStatus == GL_TRUE) { + return true; + } + + GLint infoLogLength = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength); + std::vector infoLog(static_cast(std::max(infoLogLength, 1)), '\0'); + glGetProgramInfoLog(program, infoLogLength, nullptr, infoLog.data()); + if (errorMessage != nullptr) { + *errorMessage = std::string("ERROR::PROGRAM_LINKING_ERROR\n") + infoLog.data(); + } + return false; +} + +bool CompileOpenGLShaderHandleFromSource(const ShaderCompileDesc& desc, + GLuint& outShaderHandle, + ShaderType& outShaderType, + std::string* errorMessage) { + std::string sourceText; + if (!LoadShaderSourceText(desc, sourceText) || sourceText.empty()) { + if (errorMessage != nullptr) { + *errorMessage = "Failed to load GLSL shader source for OpenGL compilation."; + } + return false; + } + + if (!ResolveOpenGLShaderType(desc, sourceText, outShaderType)) { + if (errorMessage != nullptr) { + *errorMessage = "Failed to resolve OpenGL GLSL shader stage from profile, file extension, or source text."; + } + return false; + } + + GLenum glShaderType = 0; + if (!TryConvertToOpenGLShaderType(outShaderType, glShaderType)) { + if (errorMessage != nullptr) { + *errorMessage = "OpenGL does not support the requested GLSL shader stage."; + } + return false; + } + + const char* sourcePtr = sourceText.c_str(); + const GLuint shaderHandle = glCreateShader(glShaderType); + glShaderSource(shaderHandle, 1, &sourcePtr, nullptr); + glCompileShader(shaderHandle); + if (!CheckShaderCompileStatus(shaderHandle, ShaderStageLabel(outShaderType), errorMessage)) { + glDeleteShader(shaderHandle); + return false; + } + + outShaderHandle = shaderHandle; + return true; +} + +bool CompileOpenGLShaderHandleFromSpirv(const ShaderCompileDesc& desc, + GLuint& outShaderHandle, + ShaderType& outShaderType, + std::string* errorMessage) { + CompiledSpirvShader compiledShader = {}; + if (!CompileSpirvShader(desc, SpirvTargetEnvironment::Vulkan, compiledShader, errorMessage)) { + return false; + } + + std::string glslSource; + if (!TranspileSpirvToOpenGLGLSL(compiledShader, glslSource, errorMessage)) { + return false; + } + + GLenum glShaderType = 0; + if (!TryConvertToOpenGLShaderType(compiledShader.type, glShaderType)) { + if (errorMessage != nullptr) { + *errorMessage = "OpenGL does not support the requested shader stage after SPIR-V transpilation."; + } + return false; + } + + const char* sourcePtr = glslSource.c_str(); + const GLuint shaderHandle = glCreateShader(glShaderType); + glShaderSource(shaderHandle, 1, &sourcePtr, nullptr); + glCompileShader(shaderHandle); + if (!CheckShaderCompileStatus(shaderHandle, ShaderStageLabel(compiledShader.type), errorMessage)) { + glDeleteShader(shaderHandle); + return false; + } + + outShaderHandle = shaderHandle; + outShaderType = compiledShader.type; + return true; +} + +bool CompileOpenGLShaderHandle(const ShaderCompileDesc& desc, + GLuint& outShaderHandle, + ShaderType& outShaderType, + std::string* errorMessage) { + outShaderHandle = 0; + outShaderType = ShaderType::Vertex; + + if (desc.sourceLanguage == ShaderLanguage::GLSL) { + return CompileOpenGLShaderHandleFromSource(desc, outShaderHandle, outShaderType, errorMessage); + } + + if (desc.sourceLanguage == ShaderLanguage::HLSL || desc.sourceLanguage == ShaderLanguage::SPIRV) { + return CompileOpenGLShaderHandleFromSpirv(desc, outShaderHandle, outShaderType, errorMessage); + } + + if (!desc.fileName.empty()) { + const std::wstring extension = std::filesystem::path(desc.fileName).extension().wstring(); + if (extension == L".hlsl" || extension == L".spv") { + return CompileOpenGLShaderHandleFromSpirv(desc, outShaderHandle, outShaderType, errorMessage); + } + } + + return CompileOpenGLShaderHandleFromSource(desc, outShaderHandle, outShaderType, errorMessage); +} + +bool BuildOpenGLProgram(const ShaderCompileDesc* shaderDescs, + size_t shaderDescCount, + GLuint& outProgram, + ShaderType& outProgramType, + std::string* errorMessage) { + outProgram = 0; + outProgramType = ShaderType::Vertex; + if (shaderDescs == nullptr || shaderDescCount == 0) { + if (errorMessage != nullptr) { + *errorMessage = "No OpenGL shader descriptors were provided."; + } + return false; + } + + GLuint program = glCreateProgram(); + std::vector shaderHandles; + shaderHandles.reserve(shaderDescCount); + + for (size_t index = 0; index < shaderDescCount; ++index) { + GLuint shaderHandle = 0; + ShaderType shaderType = ShaderType::Vertex; + if (!CompileOpenGLShaderHandle(shaderDescs[index], shaderHandle, shaderType, errorMessage)) { + for (GLuint handle : shaderHandles) { + glDetachShader(program, handle); + glDeleteShader(handle); + } + glDeleteProgram(program); + return false; + } + + glAttachShader(program, shaderHandle); + shaderHandles.push_back(shaderHandle); + if (index == 0) { + outProgramType = shaderType; + } + } + + glLinkProgram(program); + const bool linked = CheckProgramLinkStatus(program, errorMessage); + for (GLuint handle : shaderHandles) { + glDetachShader(program, handle); + glDeleteShader(handle); + } + + if (!linked) { + glDeleteProgram(program); + return false; + } + + outProgram = program; + return true; +} + OpenGLFormat ToOpenGLTextureFormat(Format format) { switch (format) { case Format::R8_UNorm: @@ -740,30 +1145,45 @@ RHIShader* OpenGLDevice::CreateShader(const ShaderCompileDesc& desc) { return nullptr; } + if (desc.source.empty() && desc.fileName.empty()) { + delete shader; + return nullptr; + } + if (desc.sourceLanguage == ShaderLanguage::GLSL && !desc.source.empty()) { const std::string entryPoint = NarrowAscii(desc.entryPoint); - std::string profile = NarrowAscii(desc.profile); + const std::string profile = NarrowAscii(desc.profile); const char* entryPointPtr = entryPoint.empty() ? nullptr : entryPoint.c_str(); const char* profilePtr = profile.empty() ? nullptr : profile.c_str(); if (shader->Compile(desc.source.data(), desc.source.size(), entryPointPtr, profilePtr)) { return shader; } - delete shader; - return nullptr; - } - - if (!desc.fileName.empty()) { - std::wstring filePath = desc.fileName; - std::string entryPoint = NarrowAscii(desc.entryPoint); - std::string profile = NarrowAscii(desc.profile); - if (shader->CompileFromFile(filePath.c_str(), entryPoint.c_str(), profile.c_str())) { + } else if (!UsesSpirvCompilationPath(desc) && + (desc.sourceLanguage == ShaderLanguage::Unknown || desc.sourceLanguage == ShaderLanguage::GLSL) && + !desc.fileName.empty()) { + const std::string entryPoint = NarrowAscii(desc.entryPoint); + const std::string profile = NarrowAscii(desc.profile); + if (shader->CompileFromFile(desc.fileName.c_str(), entryPoint.c_str(), profile.c_str())) { return shader; } - delete shader; - return nullptr; + } else { + GLuint program = 0; + ShaderType shaderType = ShaderType::Vertex; + std::string errorMessage; + if (BuildOpenGLProgram(&desc, 1, program, shaderType, &errorMessage) && + shader->AdoptProgram(program, shaderType)) { + return shader; + } + + if (!errorMessage.empty()) { + XCEngine::Debug::Logger::Get().Error(XCEngine::Debug::LogCategory::Rendering, errorMessage.c_str()); + } + if (program != 0) { + glDeleteProgram(program); + } } - + delete shader; return nullptr; } @@ -796,28 +1216,23 @@ RHIPipelineState* OpenGLDevice::CreatePipelineState(const GraphicsPipelineDesc& } auto graphicsShader = std::make_unique(); - bool compiled = false; - if (!desc.vertexShader.source.empty() && !desc.fragmentShader.source.empty()) { - const std::string vertexSource = SourceToString(desc.vertexShader); - const std::string fragmentSource = SourceToString(desc.fragmentShader); - if (hasGeometryShader && !desc.geometryShader.source.empty()) { - const std::string geometrySource = SourceToString(desc.geometryShader); - compiled = graphicsShader->Compile(vertexSource.c_str(), fragmentSource.c_str(), geometrySource.c_str()); - } else { - compiled = graphicsShader->Compile(vertexSource.c_str(), fragmentSource.c_str()); + ShaderCompileDesc shaderDescs[3] = { + desc.vertexShader, + desc.fragmentShader, + desc.geometryShader + }; + const size_t shaderDescCount = hasGeometryShader ? 3u : 2u; + GLuint program = 0; + ShaderType programType = ShaderType::Vertex; + std::string errorMessage; + if (!BuildOpenGLProgram(shaderDescs, shaderDescCount, program, programType, &errorMessage) || + !graphicsShader->AdoptProgram(program, programType)) { + if (!errorMessage.empty()) { + XCEngine::Debug::Logger::Get().Error(XCEngine::Debug::LogCategory::Rendering, errorMessage.c_str()); } - } else if (!desc.vertexShader.fileName.empty() && !desc.fragmentShader.fileName.empty()) { - const std::string vertexPath = NarrowAscii(desc.vertexShader.fileName); - const std::string fragmentPath = NarrowAscii(desc.fragmentShader.fileName); - if (hasGeometryShader && !desc.geometryShader.fileName.empty()) { - const std::string geometryPath = NarrowAscii(desc.geometryShader.fileName); - compiled = graphicsShader->CompileFromFile(vertexPath.c_str(), fragmentPath.c_str(), geometryPath.c_str()); - } else { - compiled = graphicsShader->CompileFromFile(vertexPath.c_str(), fragmentPath.c_str()); + if (program != 0) { + glDeleteProgram(program); } - } - - if (!compiled) { delete pso; return nullptr; } diff --git a/engine/src/RHI/OpenGL/OpenGLShader.cpp b/engine/src/RHI/OpenGL/OpenGLShader.cpp index 585345b5..07090ee6 100644 --- a/engine/src/RHI/OpenGL/OpenGLShader.cpp +++ b/engine/src/RHI/OpenGL/OpenGLShader.cpp @@ -374,6 +374,22 @@ bool OpenGLShader::Compile(const void* sourceData, size_t sourceSize, const char return Compile(source.c_str(), type); } +bool OpenGLShader::AdoptProgram(unsigned int program, ShaderType type) { + if (program == 0) { + return false; + } + + if (m_program != 0 && m_program != program) { + glDeleteProgram(m_program); + } + + m_program = program; + m_type = type; + m_uniformInfos.clear(); + m_uniformsCached = false; + return true; +} + void OpenGLShader::Shutdown() { if (m_program) { glDeleteProgram(m_program); diff --git a/engine/src/RHI/ShaderCompiler/SpirvShaderCompiler.cpp b/engine/src/RHI/ShaderCompiler/SpirvShaderCompiler.cpp new file mode 100644 index 00000000..4ac64df5 --- /dev/null +++ b/engine/src/RHI/ShaderCompiler/SpirvShaderCompiler.cpp @@ -0,0 +1,936 @@ +#include "XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +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 WidenAscii(const char* value) { + if (value == nullptr) { + return {}; + } + + const std::string ascii(value); + return std::wstring(ascii.begin(), ascii.end()); +} + +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; +} + +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; +} + +bool FindSpirvCross(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"spirv-cross.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"spirv-cross.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"spirv-cross.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); +} + +std::wstring ShaderSourceExtension(ShaderType type, bool isHlsl) { + if (isHlsl) { + switch (type) { + case ShaderType::Vertex: + return L".vert.hlsl"; + case ShaderType::Fragment: + return L".frag.hlsl"; + case ShaderType::Geometry: + return L".geom.hlsl"; + case ShaderType::TessControl: + return L".tesc.hlsl"; + case ShaderType::TessEvaluation: + return L".tese.hlsl"; + case ShaderType::Compute: + return L".comp.hlsl"; + default: + return L".shader.hlsl"; + } + } + + return ShaderStageExtension(type); +} + +std::wstring SpirvTargetArguments(SpirvTargetEnvironment targetEnvironment) { + switch (targetEnvironment) { + case SpirvTargetEnvironment::OpenGL: + return L"-G"; + case SpirvTargetEnvironment::Vulkan: + default: + return L"-V --target-env vulkan1.0"; + } +} + +const char* SpirvTargetName(SpirvTargetEnvironment targetEnvironment) { + switch (targetEnvironment) { + case SpirvTargetEnvironment::OpenGL: + return "OpenGL"; + case SpirvTargetEnvironment::Vulkan: + default: + return "Vulkan"; + } +} + +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; +} + +bool WriteBinaryFile(const std::filesystem::path& path, const void* data, size_t size) { + if (data == nullptr || size == 0) { + return false; + } + + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file.is_open()) { + return false; + } + + file.write(reinterpret_cast(data), static_cast(size)); + return file.good(); +} + +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, + SpirvTargetEnvironment targetEnvironment, + 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(ShaderSourceExtension(type, false).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 = + SpirvTargetArguments(targetEnvironment) + L" -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() + ? std::string("glslangValidator.exe failed to compile GLSL to SPIR-V for ") + + SpirvTargetName(targetEnvironment) + "." + : 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; +} + +bool CompileHlslToSpirv(const ShaderCompileDesc& desc, + const std::string& sourceText, + SpirvTargetEnvironment targetEnvironment, + 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(ShaderSourceExtension(type, true).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 HLSL shader file."; + } + return false; + } + + const std::wstring arguments = + L"-D --auto-map-bindings " + SpirvTargetArguments(targetEnvironment) + + L" -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() + ? std::string("glslangValidator.exe failed to compile HLSL to SPIR-V for ") + + SpirvTargetName(targetEnvironment) + "." + : 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 CompileSpirvShader(const ShaderCompileDesc& desc, + SpirvTargetEnvironment targetEnvironment, + CompiledSpirvShader& 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; + } + + 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 = IsHlslInput(desc) + ? "Failed to read HLSL shader file." + : "Failed to read GLSL shader file."; + } + return false; + } + + if (sourceText.empty()) { + if (errorMessage != nullptr) { + *errorMessage = IsHlslInput(desc) + ? "HLSL shader source is empty." + : "GLSL shader source is empty."; + } + return false; + } + + if (!ResolveShaderType(desc, sourceText, outShader.type)) { + if (errorMessage != nullptr) { + *errorMessage = std::string("Failed to resolve ") + SpirvTargetName(targetEnvironment) + + " shader stage from profile, file extension, or source text."; + } + return false; + } + + if (IsHlslInput(desc)) { + if (!CompileHlslToSpirv(desc, + sourceText, + targetEnvironment, + outShader.type, + outShader.entryPoint, + outShader.spirvWords, + errorMessage)) { + return false; + } + } else { + if (!CompileGlslToSpirv(desc, + sourceText, + targetEnvironment, + 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; +} + +bool TranspileSpirvToOpenGLGLSL(const CompiledSpirvShader& shader, + std::string& outSourceText, + std::string* errorMessage) { + outSourceText.clear(); + if (shader.spirvWords.empty()) { + if (errorMessage != nullptr) { + *errorMessage = "No SPIR-V payload was provided for GLSL transpilation."; + } + return false; + } + + std::wstring spirvCrossPath; + if (!FindSpirvCross(spirvCrossPath)) { + if (errorMessage != nullptr) { + *errorMessage = "spirv-cross.exe was not found. Install Vulkan SDK or add it to PATH."; + } + return false; + } + + std::wstring tempSpirvPath; + std::wstring tempGlslPath; + if (!CreateTemporaryPath(L".spv", tempSpirvPath) || + !CreateTemporaryPath(L".glsl", tempGlslPath)) { + if (errorMessage != nullptr) { + *errorMessage = "Failed to allocate temporary files for SPIR-V GLSL transpilation."; + } + return false; + } + + const bool wroteSpirv = WriteBinaryFile( + std::filesystem::path(tempSpirvPath), + shader.spirvWords.data(), + shader.spirvWords.size() * sizeof(uint32_t)); + if (!wroteSpirv) { + DeleteFileW(tempSpirvPath.c_str()); + DeleteFileW(tempGlslPath.c_str()); + if (errorMessage != nullptr) { + *errorMessage = "Failed to write temporary SPIR-V input for spirv-cross."; + } + return false; + } + + const std::wstring arguments = + L"--version 430 --output \"" + tempGlslPath + L"\" \"" + tempSpirvPath + L"\""; + + DWORD exitCode = 0; + std::string toolOutput; + const bool ranProcess = RunProcessAndCapture(spirvCrossPath, arguments, exitCode, toolOutput); + + std::string glslSource; + const bool loadedOutput = ranProcess && exitCode == 0 && + LoadTextFile(std::filesystem::path(tempGlslPath), glslSource); + + DeleteFileW(tempSpirvPath.c_str()); + DeleteFileW(tempGlslPath.c_str()); + + if (!ranProcess) { + if (errorMessage != nullptr) { + *errorMessage = "Failed to launch spirv-cross.exe."; + } + return false; + } + + if (!loadedOutput || glslSource.empty()) { + if (errorMessage != nullptr) { + *errorMessage = toolOutput.empty() + ? "spirv-cross.exe failed to transpile SPIR-V to OpenGL GLSL." + : toolOutput; + } + return false; + } + + outSourceText = std::move(glslSource); + return true; +} + +} // namespace RHI +} // namespace XCEngine diff --git a/engine/src/RHI/Vulkan/VulkanShaderCompiler.cpp b/engine/src/RHI/Vulkan/VulkanShaderCompiler.cpp index ef8852b1..e0425feb 100644 --- a/engine/src/RHI/Vulkan/VulkanShaderCompiler.cpp +++ b/engine/src/RHI/Vulkan/VulkanShaderCompiler.cpp @@ -1,721 +1,12 @@ #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); -} - -std::wstring ShaderSourceExtension(ShaderType type, bool isHlsl) { - if (isHlsl) { - switch (type) { - case ShaderType::Vertex: - return L".vert.hlsl"; - case ShaderType::Fragment: - return L".frag.hlsl"; - case ShaderType::Geometry: - return L".geom.hlsl"; - case ShaderType::TessControl: - return L".tesc.hlsl"; - case ShaderType::TessEvaluation: - return L".tese.hlsl"; - case ShaderType::Compute: - return L".comp.hlsl"; - default: - return L".shader.hlsl"; - } - } - - return ShaderStageExtension(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(ShaderSourceExtension(type, false).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; -} - -bool CompileHlslToSpirv(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(ShaderSourceExtension(type, true).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 HLSL shader file."; - } - return false; - } - - const std::wstring arguments = - L"-D -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 HLSL 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; - } - - 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 = IsHlslInput(desc) - ? "Failed to read HLSL shader file." - : "Failed to read GLSL shader file."; - } - return false; - } - - if (sourceText.empty()) { - if (errorMessage != nullptr) { - *errorMessage = IsHlslInput(desc) - ? "HLSL shader source is empty." - : "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 (IsHlslInput(desc)) { - if (!CompileHlslToSpirv(desc, - sourceText, - outShader.type, - outShader.entryPoint, - outShader.spirvWords, - errorMessage)) { - return false; - } - } else { - 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; + return CompileSpirvShader(desc, SpirvTargetEnvironment::Vulkan, outShader, errorMessage); } } // namespace RHI diff --git a/tests/RHI/OpenGL/unit/test_backend_specific.cpp b/tests/RHI/OpenGL/unit/test_backend_specific.cpp index f6095341..c86c5e05 100644 --- a/tests/RHI/OpenGL/unit/test_backend_specific.cpp +++ b/tests/RHI/OpenGL/unit/test_backend_specific.cpp @@ -19,10 +19,37 @@ #include #include +#include +#include #include using namespace XCEngine::RHI; +namespace { + +bool FindToolFromVulkanSdkOrPath(const wchar_t* fileName) { + wchar_t sdkBuffer[32767] = {}; + const DWORD sdkBufferCount = static_cast(sizeof(sdkBuffer) / sizeof(sdkBuffer[0])); + const DWORD sdkLength = GetEnvironmentVariableW(L"VULKAN_SDK", sdkBuffer, sdkBufferCount); + if (sdkLength > 0 && sdkLength < sdkBufferCount) { + const std::filesystem::path candidate = std::filesystem::path(sdkBuffer) / L"Bin" / fileName; + if (std::filesystem::exists(candidate)) { + return true; + } + } + + wchar_t pathBuffer[MAX_PATH] = {}; + const DWORD pathLength = SearchPathW(nullptr, fileName, nullptr, MAX_PATH, pathBuffer, nullptr); + return pathLength > 0 && pathLength < MAX_PATH; +} + +bool SupportsOpenGLHlslToolchainForTests() { + return FindToolFromVulkanSdkOrPath(L"glslangValidator.exe") && + FindToolFromVulkanSdkOrPath(L"spirv-cross.exe"); +} + +} // namespace + TEST_F(OpenGLTestFixture, Device_Initialize_UnifiedPath_CanCreateSwapChain) { auto* device = new OpenGLDevice(); ASSERT_NE(device, nullptr); @@ -52,6 +79,100 @@ TEST_F(OpenGLTestFixture, Device_Initialize_UnifiedPath_CanCreateSwapChain) { delete device; } +TEST_F(OpenGLTestFixture, Device_CreateShader_HlslVertex_UsesTranspiledHlslPath) { + ASSERT_TRUE(GetDevice()->MakeContextCurrent()); + if (!SupportsOpenGLHlslToolchainForTests()) { + GTEST_SKIP() << "glslangValidator.exe or spirv-cross.exe was not found."; + } + + static const char* vertexSource = R"( +struct VSInput { + float4 position : POSITION; +}; + +struct VSOutput { + float4 position : SV_POSITION; +}; + +VSOutput MainVS(VSInput input) { + VSOutput output; + output.position = input.position; + return output; +} +)"; + + ShaderCompileDesc shaderDesc = {}; + shaderDesc.source.assign(vertexSource, vertexSource + std::strlen(vertexSource)); + shaderDesc.sourceLanguage = ShaderLanguage::HLSL; + shaderDesc.entryPoint = L"MainVS"; + shaderDesc.profile = L"vs_5_0"; + + RHIShader* shader = GetDevice()->CreateShader(shaderDesc); + ASSERT_NE(shader, nullptr); + EXPECT_TRUE(shader->IsValid()); + EXPECT_EQ(shader->GetType(), ShaderType::Vertex); + + shader->Shutdown(); + delete shader; +} + +TEST_F(OpenGLTestFixture, Device_CreatePipelineState_HlslGraphicsShaders_UsesTranspiledHlslPath) { + ASSERT_TRUE(GetDevice()->MakeContextCurrent()); + if (!SupportsOpenGLHlslToolchainForTests()) { + GTEST_SKIP() << "glslangValidator.exe or spirv-cross.exe was not found."; + } + + static const char* hlslSource = R"( +struct VSInput { + float4 position : POSITION; +}; + +struct PSInput { + float4 position : SV_POSITION; +}; + +PSInput MainVS(VSInput input) { + PSInput output; + output.position = input.position; + return output; +} + +float4 MainPS(PSInput input) : SV_TARGET { + return float4(1.0f, 0.25f, 0.0f, 1.0f); +} +)"; + + 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.source.assign(hlslSource, hlslSource + std::strlen(hlslSource)); + pipelineDesc.vertexShader.sourceLanguage = ShaderLanguage::HLSL; + pipelineDesc.vertexShader.entryPoint = L"MainVS"; + pipelineDesc.vertexShader.profile = L"vs_5_0"; + + pipelineDesc.fragmentShader.source.assign(hlslSource, hlslSource + std::strlen(hlslSource)); + pipelineDesc.fragmentShader.sourceLanguage = ShaderLanguage::HLSL; + pipelineDesc.fragmentShader.entryPoint = L"MainPS"; + pipelineDesc.fragmentShader.profile = L"ps_5_0"; + + RHIPipelineState* pipelineState = GetDevice()->CreatePipelineState(pipelineDesc); + ASSERT_NE(pipelineState, nullptr); + EXPECT_NE(pipelineState->GetNativeHandle(), nullptr); + + pipelineState->Shutdown(); + delete pipelineState; +} + TEST_F(OpenGLTestFixture, CommandList_SetRenderTargets_BindsColorAndDepthAttachments) { TextureDesc colorDesc = {}; colorDesc.width = 128;