mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
feat: Integrate shaderc for shader compilation and enhance shader file handling
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user