diff --git a/CMakeLists.txt b/CMakeLists.txt index 790d75a..3956514 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,6 +115,9 @@ endif() if(BUILD_SDL3_APP) find_package(Vulkan REQUIRED) + if(NOT ENABLE_VITA) + find_package(shaderc CONFIG REQUIRED) + endif() endif() # SDL is required for both the demo app and cube_script_tests (used by audio_player) @@ -193,6 +196,15 @@ if(BUILD_SDL3_APP) Vorbis::vorbis cpptrace::cpptrace ) + if(NOT ENABLE_VITA) + if(TARGET shaderc::shaderc) + target_link_libraries(sdl3_app PRIVATE shaderc::shaderc) + elseif(TARGET shaderc::shaderc_combined) + target_link_libraries(sdl3_app PRIVATE shaderc::shaderc_combined) + else() + message(FATAL_ERROR "shaderc CMake target not found") + endif() + endif() target_compile_definitions(sdl3_app PRIVATE SDL_MAIN_HANDLED) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/shaders") diff --git a/scripts/dev_commands.py b/scripts/dev_commands.py index 80e925e..3efe1e1 100644 --- a/scripts/dev_commands.py +++ b/scripts/dev_commands.py @@ -281,68 +281,6 @@ def msvc_quick(args: argparse.Namespace) -> None: run_argvs([cmd], args.dry_run) -def _compile_shaders(dry_run: bool) -> None: - """ - Compile GLSL shaders to SPIR-V format using glslangValidator. - Compiles .vert, .frag, .geom, .tesc, .tese, and .comp files in the shaders directory. - """ - shaders_dir = Path("shaders") - if not shaders_dir.exists(): - return - - # Find shader compiler - compiler = None - for cmd in ["glslangValidator", "glslc"]: - try: - result = subprocess.run([cmd, "--version"], - capture_output=True, - timeout=5) - if result.returncode == 0: - compiler = cmd - break - except (FileNotFoundError, subprocess.TimeoutExpired): - continue - - if not compiler: - print("⚠️ No shader compiler found (glslangValidator or glslc)") - print(" Skipping shader compilation") - return - - print("\n=== Compiling Shaders ===") - 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") - - # Check if compilation is needed - if output_file.exists(): - if output_file.stat().st_mtime >= shader_file.stat().st_mtime: - continue # Skip if .spv is newer than source - - print(f" Compiling {shader_file.name} -> {output_file.name}") - - if not dry_run: - if compiler == "glslangValidator": - cmd = [compiler, "-V", str(shader_file), "-o", str(output_file)] - else: # glslc - cmd = [compiler, str(shader_file), "-o", str(output_file)] - - result = subprocess.run(cmd, capture_output=True, text=True) - if result.returncode != 0: - print(f" ❌ Failed: {result.stderr}") - else: - print(f" ✓ Success") - - print("=== Shaders Compiled ===\n") - - def _sync_assets(build_dir: str, dry_run: bool) -> None: """ Sync asset files (scripts, shaders, models) from the project root to the @@ -356,7 +294,7 @@ def _sync_assets(build_dir: str, dry_run: bool) -> None: # Define asset directories to sync asset_dirs = [ ("scripts", ["*.lua"]), - ("shaders", ["*.spv"]), + ("shaders", ["*.vert", "*.frag", "*.geom", "*.tesc", "*.tese", "*.comp", "*.spv"]), ("scripts/models", ["*.stl", "*.obj", "*.fbx"]), ("config", ["*.json"]), ] @@ -392,14 +330,12 @@ def run_demo(args: argparse.Namespace) -> None: executable is `sdl3_app` (or `sdl3_app.exe` on Windows). Additional arguments can be passed to the executable after `--`. - By default, compiles shaders and syncs asset files before running. - Use --no-sync to skip shader compilation and asset synchronization. + By default, syncs asset files before running. + Use --no-sync to skip asset synchronization. """ build_dir = _as_build_dir(args.build_dir, DEFAULT_BUILD_DIR) - # Compile shaders and sync assets unless --no-sync is specified if not args.no_sync: - _compile_shaders(args.dry_run) _sync_assets(build_dir, args.dry_run) exe_name = args.target or ("sdl3_app.exe" if IS_WINDOWS else "sdl3_app") @@ -1115,9 +1051,9 @@ def gui(args: argparse.Namespace) -> None: build_action.triggered.connect(self.run_build) dev_menu.addAction(build_action) - shader_action = QAction("Compile Shaders", self) - shader_action.triggered.connect(self.compile_shaders) - dev_menu.addAction(shader_action) + sync_action = QAction("Sync Assets", self) + sync_action.triggered.connect(self.sync_assets) + dev_menu.addAction(sync_action) dev_menu.addSeparator() @@ -1660,12 +1596,16 @@ return {{ ] self.run_command(cmd) - def compile_shaders(self): - """Compile shaders manually""" + def sync_assets(self): + """Sync assets into the active build directory""" + if self.preset != "default": + build_dir = f"build-{self.preset.split('-')[0]}" # e.g., build-vita + else: + build_dir = GENERATOR_DEFAULT_DIR.get(self.generator, DEFAULT_BUILD_DIR) self.console.clear() - self.log("=== Compiling Shaders ===\n") - _compile_shaders(dry_run=False) - self.log("\n✓ Shader compilation completed") + self.log("=== Syncing Assets ===\n") + _sync_assets(build_dir, dry_run=False) + self.log("\n✓ Asset sync completed") app = QApplication(sys.argv) window = BuildLauncherGUI() @@ -1787,7 +1727,7 @@ def main() -> int: runp.add_argument( "--no-sync", action="store_true", - help="skip shader compilation and asset syncing before running", + help="skip asset syncing before running", ) runp.add_argument( "args", diff --git a/src/services/impl/pipeline_service.cpp b/src/services/impl/pipeline_service.cpp index 048735a..454feba 100644 --- a/src/services/impl/pipeline_service.cpp +++ b/src/services/impl/pipeline_service.cpp @@ -1,11 +1,39 @@ #include "pipeline_service.hpp" #include "../../core/vertex.hpp" +#include #include +#include #include #include +#include #include #include +namespace { +bool IsSpirvPath(const std::filesystem::path& path) { + return path.extension() == ".spv"; +} + +shaderc_shader_kind ShadercKindFromStage(VkShaderStageFlagBits stage) { + switch (stage) { + case VK_SHADER_STAGE_VERTEX_BIT: + return shaderc_vertex_shader; + case VK_SHADER_STAGE_FRAGMENT_BIT: + return shaderc_fragment_shader; + case VK_SHADER_STAGE_GEOMETRY_BIT: + return shaderc_geometry_shader; + case VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT: + return shaderc_tess_control_shader; + case VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT: + return shaderc_tess_evaluation_shader; + case VK_SHADER_STAGE_COMPUTE_BIT: + return shaderc_compute_shader; + default: + return shaderc_glsl_infer_from_source; + } +} +} // namespace + namespace sdl3cpp::services::impl { PipelineService::PipelineService(std::shared_ptr deviceService, std::shared_ptr logger) @@ -235,45 +263,35 @@ void PipelineService::CreatePipelinesInternal(VkRenderPass renderPass, VkExtent2 // Create pipeline for each registered shader for (const auto& [key, paths] : shaderPathMap_) { + auto requireShader = [&](const std::string& label, const std::string& path) { + if (!HasShaderSource(path)) { + throw std::runtime_error( + label + " shader not found: " + path + + "\n\nShader key: " + key + + "\n\nPlease ensure the shader source (.vert/.frag/etc.) or compiled .spv exists."); + } + }; + // Validate shader files exist - if (!std::filesystem::exists(paths.vertex)) { - throw std::runtime_error( - "Vertex shader not found: " + paths.vertex + - "\n\nShader key: " + key + - "\n\nPlease ensure shader files are compiled and present in the shaders directory."); - } - if (!std::filesystem::exists(paths.fragment)) { - throw std::runtime_error( - "Fragment shader not found: " + paths.fragment + - "\n\nShader key: " + key + - "\n\nPlease ensure shader files are compiled and present in the shaders directory."); - } + requireShader("Vertex", paths.vertex); + requireShader("Fragment", paths.fragment); bool hasGeometry = !paths.geometry.empty(); bool hasTessControl = !paths.tessControl.empty(); bool hasTessEval = !paths.tessEval.empty(); - 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 (hasGeometry) { + requireShader("Geometry", paths.geometry); } 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 (hasTessControl) { + requireShader("Tessellation control", paths.tessControl); } - 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."); + if (hasTessEval) { + requireShader("Tessellation evaluation", paths.tessEval); } std::vector shaderModules; @@ -288,7 +306,7 @@ void PipelineService::CreatePipelinesInternal(VkRenderPass renderPass, VkExtent2 }; auto addStage = [&](VkShaderStageFlagBits stage, const std::string& path) { - auto shaderCode = ReadShaderFile(path); + auto shaderCode = ReadShaderFile(path, stage); VkShaderModule shaderModule = CreateShaderModule(shaderCode); shaderModules.push_back(shaderModule); @@ -373,33 +391,97 @@ VkShaderModule PipelineService::CreateShaderModule(const std::vector& code return shaderModule; } -std::vector PipelineService::ReadShaderFile(const std::string& path) { - logger_->Trace("PipelineService", "ReadShaderFile", "path=" + path); +bool PipelineService::HasShaderSource(const std::string& path) const { + if (path.empty()) { + return false; + } + std::filesystem::path shaderPath(path); + if (std::filesystem::exists(shaderPath)) { + return true; + } + if (IsSpirvPath(shaderPath)) { + std::filesystem::path sourcePath = shaderPath; + sourcePath.replace_extension(); + return std::filesystem::exists(sourcePath); + } + return false; +} - if (!std::filesystem::exists(path)) { - throw std::runtime_error("Shader file not found: " + path + - "\n\nPlease ensure the file exists at this location."); +std::vector PipelineService::ReadShaderFile(const std::string& path, VkShaderStageFlagBits stage) { + logger_->Trace("PipelineService", "ReadShaderFile", + "path=" + path + ", stage=" + std::to_string(static_cast(stage))); + + if (path.empty()) { + throw std::runtime_error("Shader path is empty"); } - if (!std::filesystem::is_regular_file(path)) { - throw std::runtime_error("Path is not a regular file: " + path); + std::filesystem::path shaderPath(path); + if (!std::filesystem::exists(shaderPath) && IsSpirvPath(shaderPath)) { + std::filesystem::path sourcePath = shaderPath; + sourcePath.replace_extension(); + if (std::filesystem::exists(sourcePath)) { + logger_->Trace("PipelineService", "ReadShaderFile", + "usingSource=" + sourcePath.string()); + shaderPath = sourcePath; + } } - std::ifstream file(path, std::ios::ate | std::ios::binary); - if (!file) { - throw std::runtime_error("Failed to open shader file: " + path + - "\n\nCheck file permissions."); + if (!std::filesystem::exists(shaderPath)) { + throw std::runtime_error("Shader file not found: " + shaderPath.string() + + "\n\nPlease ensure the source (.vert/.frag/etc.) or compiled .spv exists."); } - size_t fileSize = static_cast(file.tellg()); - std::vector buffer(fileSize); + if (!std::filesystem::is_regular_file(shaderPath)) { + throw std::runtime_error("Path is not a regular file: " + shaderPath.string()); + } - file.seekg(0); - file.read(buffer.data(), static_cast(fileSize)); - file.close(); + if (IsSpirvPath(shaderPath)) { + std::ifstream file(shaderPath, std::ios::ate | std::ios::binary); + if (!file) { + throw std::runtime_error("Failed to open shader file: " + shaderPath.string() + + "\n\nCheck file permissions."); + } - logger_->Debug("Read shader file: " + path + " (" + std::to_string(fileSize) + " bytes)"); + size_t fileSize = static_cast(file.tellg()); + std::vector buffer(fileSize); + file.seekg(0); + file.read(buffer.data(), static_cast(fileSize)); + file.close(); + + logger_->Debug("Read shader file: " + shaderPath.string() + + " (" + std::to_string(fileSize) + " bytes)"); + return buffer; + } + + std::ifstream sourceFile(shaderPath); + if (!sourceFile) { + throw std::runtime_error("Failed to open shader source: " + shaderPath.string()); + } + std::string source((std::istreambuf_iterator(sourceFile)), + std::istreambuf_iterator()); + sourceFile.close(); + + shaderc::Compiler compiler; + shaderc::CompileOptions options; + options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2); + + shaderc_shader_kind kind = ShadercKindFromStage(stage); + auto result = compiler.CompileGlslToSpv(source, kind, shaderPath.string().c_str(), options); + if (result.GetCompilationStatus() != shaderc_compilation_status_success) { + std::string error = result.GetErrorMessage(); + logger_->Error("Shader compilation failed: " + shaderPath.string() + "\n" + error); + throw std::runtime_error("Shader compilation failed: " + shaderPath.string() + "\n" + error); + } + + std::vector spirv(result.cbegin(), result.cend()); + std::vector buffer(spirv.size() * sizeof(uint32_t)); + if (!buffer.empty()) { + std::memcpy(buffer.data(), spirv.data(), buffer.size()); + } + + logger_->Debug("Compiled shader: " + shaderPath.string() + + " (" + std::to_string(buffer.size()) + " bytes)"); return buffer; } diff --git a/src/services/impl/pipeline_service.hpp b/src/services/impl/pipeline_service.hpp index 854520f..ac478b3 100644 --- a/src/services/impl/pipeline_service.hpp +++ b/src/services/impl/pipeline_service.hpp @@ -52,7 +52,8 @@ private: // Helper methods VkShaderModule CreateShaderModule(const std::vector& code); - std::vector ReadShaderFile(const std::string& path); + std::vector ReadShaderFile(const std::string& path, VkShaderStageFlagBits stage); + bool HasShaderSource(const std::string& path) const; void CreatePipelineLayout(); void CreatePipelinesInternal(VkRenderPass renderPass, VkExtent2D extent); void CleanupPipelines();