diff --git a/conanfile.py b/conanfile.py index 6269256..d9f636c 100644 --- a/conanfile.py +++ b/conanfile.py @@ -37,6 +37,8 @@ class SDL3CppConan(ConanFile): self.requires("vulkan-headers/1.3.243.0") self.requires("vulkan-memory-allocator/3.3.0") self.requires("vulkan-validationlayers/1.3.243.0") + self.requires("spirv-tools/1.4.313.0") + self.requires("spirv-headers/1.4.313.0") self.requires("cpptrace/1.0.4") self.requires("ogg/1.3.5") self.requires("theora/1.1.1") diff --git a/scripts/dev_commands.py b/scripts/dev_commands.py index 748e0cd..80e925e 100644 --- a/scripts/dev_commands.py +++ b/scripts/dev_commands.py @@ -284,7 +284,7 @@ def msvc_quick(args: argparse.Namespace) -> None: def _compile_shaders(dry_run: bool) -> None: """ Compile GLSL shaders to SPIR-V format using glslangValidator. - Compiles all .vert and .frag files in the shaders directory. + Compiles .vert, .frag, .geom, .tesc, .tese, and .comp files in the shaders directory. """ shaders_dir = Path("shaders") if not shaders_dir.exists(): @@ -309,7 +309,14 @@ def _compile_shaders(dry_run: bool) -> None: return print("\n=== Compiling Shaders ===") - shader_files = list(shaders_dir.glob("*.vert")) + list(shaders_dir.glob("*.frag")) + shader_files = ( + list(shaders_dir.glob("*.vert")) + + list(shaders_dir.glob("*.frag")) + + list(shaders_dir.glob("*.geom")) + + list(shaders_dir.glob("*.tesc")) + + list(shaders_dir.glob("*.tese")) + + list(shaders_dir.glob("*.comp")) + ) for shader_file in shader_files: output_file = shader_file.with_suffix(shader_file.suffix + ".spv") diff --git a/src/services/impl/pipeline_service.cpp b/src/services/impl/pipeline_service.cpp index 48a0dbb..048735a 100644 --- a/src/services/impl/pipeline_service.cpp +++ b/src/services/impl/pipeline_service.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace sdl3cpp::services::impl { @@ -248,42 +249,96 @@ void PipelineService::CreatePipelinesInternal(VkRenderPass renderPass, VkExtent2 "\n\nPlease ensure shader files are compiled and present in the shaders directory."); } - auto vertShaderCode = ReadShaderFile(paths.vertex); - auto fragShaderCode = ReadShaderFile(paths.fragment); + bool hasGeometry = !paths.geometry.empty(); + bool hasTessControl = !paths.tessControl.empty(); + bool hasTessEval = !paths.tessEval.empty(); - VkShaderModule vertShaderModule = CreateShaderModule(vertShaderCode); - VkShaderModule fragShaderModule = CreateShaderModule(fragShaderCode); - - VkPipelineShaderStageCreateInfo vertStageInfo{}; - vertStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - vertStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; - vertStageInfo.module = vertShaderModule; - vertStageInfo.pName = "main"; - - VkPipelineShaderStageCreateInfo fragStageInfo{}; - fragStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - fragStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; - fragStageInfo.module = fragShaderModule; - fragStageInfo.pName = "main"; - - VkPipelineShaderStageCreateInfo shaderStages[] = {vertStageInfo, fragStageInfo}; - - VkGraphicsPipelineCreateInfo pipelineCreateInfo = pipelineInfo; - pipelineCreateInfo.stageCount = 2; - pipelineCreateInfo.pStages = shaderStages; - - VkPipeline pipeline; - if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, - &pipeline) != VK_SUCCESS) { - vkDestroyShaderModule(device, fragShaderModule, nullptr); - vkDestroyShaderModule(device, vertShaderModule, nullptr); - throw std::runtime_error("Failed to create graphics pipeline for shader: " + key); + if (hasGeometry && !std::filesystem::exists(paths.geometry)) { + throw std::runtime_error( + "Geometry shader not found: " + paths.geometry + + "\n\nShader key: " + key + + "\n\nPlease ensure shader files are compiled and present in the shaders directory."); + } + if (hasTessControl != hasTessEval) { + throw std::runtime_error( + "Tessellation shaders require both 'tesc' and 'tese' paths. Shader key: " + key); + } + if (hasTessControl && !std::filesystem::exists(paths.tessControl)) { + throw std::runtime_error( + "Tessellation control shader not found: " + paths.tessControl + + "\n\nShader key: " + key + + "\n\nPlease ensure shader files are compiled and present in the shaders directory."); + } + if (hasTessEval && !std::filesystem::exists(paths.tessEval)) { + throw std::runtime_error( + "Tessellation evaluation shader not found: " + paths.tessEval + + "\n\nShader key: " + key + + "\n\nPlease ensure shader files are compiled and present in the shaders directory."); } - pipelines_[key] = pipeline; + std::vector shaderModules; + std::vector shaderStages; + shaderStages.reserve(2 + (hasGeometry ? 1 : 0) + (hasTessControl ? 2 : 0)); - vkDestroyShaderModule(device, fragShaderModule, nullptr); - vkDestroyShaderModule(device, vertShaderModule, nullptr); + auto destroyShaderModules = [&]() { + for (VkShaderModule module : shaderModules) { + vkDestroyShaderModule(device, module, nullptr); + } + shaderModules.clear(); + }; + + auto addStage = [&](VkShaderStageFlagBits stage, const std::string& path) { + auto shaderCode = ReadShaderFile(path); + VkShaderModule shaderModule = CreateShaderModule(shaderCode); + shaderModules.push_back(shaderModule); + + VkPipelineShaderStageCreateInfo stageInfo{}; + stageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stageInfo.stage = stage; + stageInfo.module = shaderModule; + stageInfo.pName = "main"; + shaderStages.push_back(stageInfo); + }; + + try { + addStage(VK_SHADER_STAGE_VERTEX_BIT, paths.vertex); + if (hasTessControl) { + addStage(VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT, paths.tessControl); + addStage(VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT, paths.tessEval); + } + if (hasGeometry) { + addStage(VK_SHADER_STAGE_GEOMETRY_BIT, paths.geometry); + } + addStage(VK_SHADER_STAGE_FRAGMENT_BIT, paths.fragment); + + VkPipelineInputAssemblyStateCreateInfo inputAssemblyState = inputAssembly; + VkPipelineTessellationStateCreateInfo tessellationState{}; + bool useTessellation = hasTessControl && hasTessEval; + if (useTessellation) { + inputAssemblyState.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; + tessellationState.sType = VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO; + tessellationState.patchControlPoints = 3; + } + + VkGraphicsPipelineCreateInfo pipelineCreateInfo = pipelineInfo; + pipelineCreateInfo.pInputAssemblyState = &inputAssemblyState; + pipelineCreateInfo.pTessellationState = useTessellation ? &tessellationState : nullptr; + pipelineCreateInfo.stageCount = static_cast(shaderStages.size()); + pipelineCreateInfo.pStages = shaderStages.data(); + + VkPipeline pipeline; + if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, + &pipeline) != VK_SUCCESS) { + destroyShaderModules(); + throw std::runtime_error("Failed to create graphics pipeline for shader: " + key); + } + + pipelines_[key] = pipeline; + destroyShaderModules(); + } catch (...) { + destroyShaderModules(); + throw; + } logger_->Debug("Created pipeline: " + key); } diff --git a/src/services/impl/shader_script_service.cpp b/src/services/impl/shader_script_service.cpp index 2617a18..76f7ccb 100644 --- a/src/services/impl/shader_script_service.cpp +++ b/src/services/impl/shader_script_service.cpp @@ -82,30 +82,52 @@ ShaderPaths ShaderScriptService::ReadShaderPathsTable(lua_State* L, int index) c ShaderPaths paths; int absIndex = lua_absindex(L, index); - lua_getfield(L, absIndex, "vertex"); - if (!lua_isstring(L, -1)) { - lua_pop(L, 1); - if (logger_) { - logger_->Error("Shader path 'vertex' must be a string"); + auto readRequiredPath = [&](const char* field, std::string& target) { + lua_getfield(L, absIndex, field); + if (!lua_isstring(L, -1)) { + lua_pop(L, 1); + if (logger_) { + logger_->Error("Shader path '" + std::string(field) + "' must be a string"); + } + throw std::runtime_error("Shader path '" + std::string(field) + "' must be a string"); } - throw std::runtime_error("Shader path 'vertex' must be a string"); - } - paths.vertex = lua_tostring(L, -1); - lua_pop(L, 1); - - lua_getfield(L, absIndex, "fragment"); - if (!lua_isstring(L, -1)) { + target = lua_tostring(L, -1); lua_pop(L, 1); - if (logger_) { - logger_->Error("Shader path 'fragment' must be a string"); - } - throw std::runtime_error("Shader path 'fragment' must be a string"); - } - paths.fragment = lua_tostring(L, -1); - lua_pop(L, 1); + }; - paths.vertex = ResolveShaderPath(paths.vertex); - paths.fragment = ResolveShaderPath(paths.fragment); + auto readOptionalPath = [&](const char* field, std::string& target) { + lua_getfield(L, absIndex, field); + if (lua_isstring(L, -1)) { + target = lua_tostring(L, -1); + } else if (!lua_isnil(L, -1)) { + lua_pop(L, 1); + if (logger_) { + logger_->Error("Shader path '" + std::string(field) + "' must be a string when provided"); + } + throw std::runtime_error("Shader path '" + std::string(field) + "' must be a string when provided"); + } + lua_pop(L, 1); + }; + + readRequiredPath("vertex", paths.vertex); + readRequiredPath("fragment", paths.fragment); + readOptionalPath("geometry", paths.geometry); + readOptionalPath("tesc", paths.tessControl); + readOptionalPath("tese", paths.tessEval); + readOptionalPath("compute", paths.compute); + + auto resolveIfPresent = [&](std::string& value) { + if (!value.empty()) { + value = ResolveShaderPath(value); + } + }; + + resolveIfPresent(paths.vertex); + resolveIfPresent(paths.fragment); + resolveIfPresent(paths.geometry); + resolveIfPresent(paths.tessControl); + resolveIfPresent(paths.tessEval); + resolveIfPresent(paths.compute); return paths; } diff --git a/src/services/interfaces/graphics_types.hpp b/src/services/interfaces/graphics_types.hpp index 295a169..7eee500 100644 --- a/src/services/interfaces/graphics_types.hpp +++ b/src/services/interfaces/graphics_types.hpp @@ -22,6 +22,10 @@ struct GraphicsConfig { struct ShaderPaths { std::string vertex; std::string fragment; + std::string geometry; + std::string tessControl; + std::string tessEval; + std::string compute; }; /** @@ -35,4 +39,4 @@ struct RenderCommand { std::array modelMatrix; }; -} // namespace sdl3cpp::services \ No newline at end of file +} // namespace sdl3cpp::services