feat: Integrate shaderc for shader compilation and enhance shader file handling

This commit is contained in:
2026-01-05 22:12:13 +00:00
parent d5e14de2d0
commit 90e7882cb2
4 changed files with 157 additions and 122 deletions

View File

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

View File

@@ -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("\nShader compilation completed")
self.log("=== Syncing Assets ===\n")
_sync_assets(build_dir, dry_run=False)
self.log("\nAsset 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",

View File

@@ -1,11 +1,39 @@
#include "pipeline_service.hpp"
#include "../../core/vertex.hpp"
#include <shaderc/shaderc.hpp>
#include <array>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <vector>
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<IVulkanDeviceService> deviceService, std::shared_ptr<ILogger> 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<VkShaderModule> 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<char>& code
return shaderModule;
}
std::vector<char> 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<char> PipelineService::ReadShaderFile(const std::string& path, VkShaderStageFlagBits stage) {
logger_->Trace("PipelineService", "ReadShaderFile",
"path=" + path + ", stage=" + std::to_string(static_cast<int>(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<size_t>(file.tellg());
std::vector<char> 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<std::streamsize>(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<size_t>(file.tellg());
std::vector<char> buffer(fileSize);
file.seekg(0);
file.read(buffer.data(), static_cast<std::streamsize>(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<char>(sourceFile)),
std::istreambuf_iterator<char>());
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<uint32_t> spirv(result.cbegin(), result.cend());
std::vector<char> 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;
}

View File

@@ -52,7 +52,8 @@ private:
// Helper methods
VkShaderModule CreateShaderModule(const std::vector<char>& code);
std::vector<char> ReadShaderFile(const std::string& path);
std::vector<char> ReadShaderFile(const std::string& path, VkShaderStageFlagBits stage);
bool HasShaderSource(const std::string& path) const;
void CreatePipelineLayout();
void CreatePipelinesInternal(VkRenderPass renderPass, VkExtent2D extent);
void CleanupPipelines();