rendering: add opengl hlsl shader translation

This commit is contained in:
2026-04-06 18:07:13 +08:00
parent f912e81ade
commit 97e986b52c
9 changed files with 1561 additions and 752 deletions

View File

@@ -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

View File

@@ -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
} // namespace XCEngine

View File

@@ -0,0 +1,33 @@
#pragma once
#include "XCEngine/RHI/RHITypes.h"
#include <cstdint>
#include <string>
#include <vector>
namespace XCEngine {
namespace RHI {
enum class SpirvTargetEnvironment : uint8_t {
Vulkan = 0,
OpenGL = 1
};
struct CompiledSpirvShader {
std::vector<uint32_t> 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

View File

@@ -1,19 +1,13 @@
#pragma once
#include "XCEngine/RHI/RHITypes.h"
#include "XCEngine/RHI/ShaderCompiler/SpirvShaderCompiler.h"
#include <cstdint>
#include <string>
#include <vector>
namespace XCEngine {
namespace RHI {
struct VulkanCompiledShader {
std::vector<uint32_t> spirvWords;
ShaderType type = ShaderType::Vertex;
std::string entryPoint;
};
using VulkanCompiledShader = CompiledSpirvShader;
bool CompileVulkanShader(const ShaderCompileDesc& desc,
VulkanCompiledShader& outShader,

View File

@@ -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 <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <vector>
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<const char*>(desc.source.data()), desc.source.size());
}
bool LoadShaderSourceText(const ShaderCompileDesc& desc, std::string& outSourceText) {
outSourceText.clear();
if (!desc.source.empty()) {
outSourceText.assign(reinterpret_cast<const char*>(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_t>(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<char>(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<char>(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<char> infoLog(static_cast<size_t>(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<char> infoLog(static_cast<size_t>(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<GLuint> 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<OpenGLShader>();
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;
}

View File

@@ -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);

View File

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

View File

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