feat(shader): Integrate bgfx_tools for shader compilation and add PipelineCompilerService

This commit is contained in:
2026-01-07 17:53:30 +00:00
parent 8362200def
commit cb0a420242
10 changed files with 452 additions and 163 deletions

View File

@@ -233,6 +233,7 @@ if(BUILD_SDL3_APP)
src/services/impl/null_gui_service.cpp
src/services/impl/bgfx_gui_service.cpp
src/services/impl/bgfx_shader_compiler.cpp
src/services/impl/pipeline_compiler_service.cpp
src/services/impl/bullet_physics_service.cpp
src/services/impl/scene_service.cpp
src/services/impl/graphics_service.cpp
@@ -265,7 +266,7 @@ if(BUILD_SDL3_APP)
elseif(TARGET shaderc::shaderc_combined)
target_link_libraries(sdl3_app PRIVATE shaderc::shaderc_combined)
else()
message(FATAL_ERROR "shaderc CMake target not found")
message(WARNING "shaderc CMake target not found; skipping link. Using local bgfx_tools for shader compilation at runtime.")
endif()
endif()
target_compile_definitions(sdl3_app PRIVATE SDL_MAIN_HANDLED
@@ -325,6 +326,7 @@ add_executable(script_engine_tests
src/services/impl/scene_service.cpp
src/events/event_bus.cpp
src/stb_image.cpp
src/services/impl/pipeline_compiler_service.cpp
)
target_include_directories(script_engine_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(script_engine_tests PRIVATE
@@ -344,7 +346,7 @@ if(TARGET shaderc::shaderc)
elseif(TARGET shaderc::shaderc_combined)
target_link_libraries(script_engine_tests PRIVATE shaderc::shaderc_combined)
else()
message(FATAL_ERROR "shaderc CMake target not found")
message(WARNING "shaderc CMake target not found; skipping link for script_engine_tests.")
endif()
add_test(NAME script_engine_tests COMMAND script_engine_tests)
@@ -353,6 +355,7 @@ add_executable(bgfx_gui_service_tests
src/services/impl/bgfx_gui_service.cpp
src/services/impl/bgfx_shader_compiler.cpp
src/services/impl/materialx_shader_generator.cpp
src/services/impl/pipeline_compiler_service.cpp
)
target_include_directories(bgfx_gui_service_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(bgfx_gui_service_tests PRIVATE
@@ -366,7 +369,7 @@ if(TARGET shaderc::shaderc)
elseif(TARGET shaderc::shaderc_combined)
target_link_libraries(bgfx_gui_service_tests PRIVATE shaderc::shaderc_combined)
else()
message(FATAL_ERROR "shaderc CMake target not found")
message(WARNING "shaderc CMake target not found; skipping link for bgfx_gui_service_tests.")
endif()
add_test(NAME bgfx_gui_service_tests COMMAND bgfx_gui_service_tests)
@@ -382,6 +385,7 @@ add_executable(vulkan_shader_linking_test
src/services/impl/sdl_window_service.cpp
src/events/event_bus.cpp
src/stb_image.cpp
src/services/impl/pipeline_compiler_service.cpp
)
target_include_directories(vulkan_shader_linking_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(vulkan_shader_linking_test PRIVATE
@@ -398,7 +402,7 @@ if(TARGET shaderc::shaderc)
elseif(TARGET shaderc::shaderc_combined)
target_link_libraries(vulkan_shader_linking_test PRIVATE shaderc::shaderc_combined)
else()
message(FATAL_ERROR "shaderc CMake target not found")
message(WARNING "shaderc CMake target not found; skipping link for vulkan_shader_linking_test.")
endif()
add_test(NAME vulkan_shader_linking_test COMMAND vulkan_shader_linking_test)
endif()

View File

@@ -17,7 +17,7 @@ class SDL3CppConan(ConanFile):
BASE_REQUIRES = (
"lua/5.4.8",
"sdl/3.2.20",
"shaderc/2025.3",
# shaderc removed; using local bgfx_tools shaderc executable instead
"cpptrace/1.0.4",
"ogg/1.3.5",
"theora/1.1.1",

View File

@@ -0,0 +1,171 @@
#include "shaderc_mem.h"
#include "shaderc.h"
#include <bx/file.h>
#include <bx/allocator.h>
#include <bx/error.h>
#include <vector>
#include <string>
#include <cstring>
#include <cstdlib>
namespace {
// Writer that captures binary output into a std::vector<uint8_t>
class MemoryWriter : public bx::FileWriter
{
public:
MemoryWriter() = default;
virtual ~MemoryWriter() {}
virtual int32_t write(const void* _data, int32_t _size, bx::Error*) override
{
const uint8_t* d = static_cast<const uint8_t*>(_data);
m_buffer.insert(m_buffer.end(), d, d + _size);
return _size;
}
const std::vector<uint8_t>& data() const { return m_buffer; }
private:
std::vector<uint8_t> m_buffer;
};
} // anonymous
int shaderc_compile_from_memory(const char* source, size_t source_len, const char* profile,
uint8_t** out_data, size_t* out_size, char** out_error) {
if (!source || source_len == 0) {
if (out_error) *out_error = strdup("empty source");
return -1;
}
try {
bgfx::Options opts;
// Map profile string (e.g., "vertex"/"fragment") into shader type
if (profile && std::strcmp(profile, "vertex") == 0) {
opts.shaderType = 'v';
} else if (profile && std::strcmp(profile, "fragment") == 0) {
opts.shaderType = 'f';
} else if (profile && std::strcmp(profile, "compute") == 0) {
opts.shaderType = 'c';
} else {
// default to fragment
opts.shaderType = 'f';
}
// Use SPIR-V target by default (matches CLI's typical default for desktop)
const uint32_t spirvVersion = 1010;
MemoryWriter writer;
bx::ErrorAssert err;
bx::WriterI* messageWriter = &writer; // reuse writer for messages as well
const std::string code(source, source_len);
bool ok = bgfx::compileSPIRVShader(opts, spirvVersion, code, &writer, messageWriter);
if (!ok) {
// capture any message buffer
const auto& msg = writer.data();
std::string s(msg.begin(), msg.end());
if (out_error) *out_error = strdup(s.c_str());
return -1;
}
const auto& out = writer.data();
if (out.empty()) {
if (out_error) *out_error = strdup("shaderc: empty output");
return -1;
}
uint8_t* buf = (uint8_t*)malloc(out.size());
if (!buf) {
if (out_error) *out_error = strdup("out of memory");
return -1;
}
memcpy(buf, out.data(), out.size());
*out_data = buf;
*out_size = out.size();
if (out_error) *out_error = nullptr;
return 0;
} catch (const std::exception& e) {
if (out_error) *out_error = strdup(e.what());
return -1;
}
}
void shaderc_free_buffer(uint8_t* data) { free(data); }
void shaderc_free_error(char* err) { free(err); }
int shaderc_compile_from_memory_with_target(const char* source, size_t source_len, const char* profile,
const char* target, uint8_t** out_data, size_t* out_size, char** out_error) {
if (!target) {
// delegate to legacy function
return shaderc_compile_from_memory(source, source_len, profile, out_data, out_size, out_error);
}
if (!source || source_len == 0) {
if (out_error) *out_error = strdup("empty source");
return -1;
}
try {
bgfx::Options opts;
if (profile && std::strcmp(profile, "vertex") == 0) {
opts.shaderType = 'v';
} else if (profile && std::strcmp(profile, "fragment") == 0) {
opts.shaderType = 'f';
} else if (profile && std::strcmp(profile, "compute") == 0) {
opts.shaderType = 'c';
} else {
opts.shaderType = 'f';
}
MemoryWriter writer;
bx::ErrorAssert err;
bx::WriterI* messageWriter = &writer;
const std::string code(source, source_len);
bool ok = false;
uint32_t version = 1010;
// Dispatch based on target language
if (0 == std::strcmp(target, "spirv")) {
ok = bgfx::compileSPIRVShader(opts, version, code, &writer, messageWriter);
} else if (0 == std::strcmp(target, "glsl")) {
ok = bgfx::compileGLSLShader(opts, version, code, &writer, messageWriter);
} else if (0 == std::strcmp(target, "msl") || 0 == std::strcmp(target, "metal")) {
ok = bgfx::compileMetalShader(opts, version, code, &writer, messageWriter);
} else if (0 == std::strcmp(target, "hlsl")) {
ok = bgfx::compileHLSLShader(opts, version, code, &writer, messageWriter);
} else {
// Unknown target: fallback to SPIR-V
ok = bgfx::compileSPIRVShader(opts, version, code, &writer, messageWriter);
}
if (!ok) {
const auto& msg = writer.data();
std::string s(msg.begin(), msg.end());
if (out_error) *out_error = strdup(s.c_str());
return -1;
}
const auto& out = writer.data();
if (out.empty()) {
if (out_error) *out_error = strdup("shaderc: empty output");
return -1;
}
uint8_t* buf = (uint8_t*)malloc(out.size());
if (!buf) {
if (out_error) *out_error = strdup("out of memory");
return -1;
}
memcpy(buf, out.data(), out.size());
*out_data = buf;
*out_size = out.size();
if (out_error) *out_error = nullptr;
return 0;
} catch (const std::exception& e) {
if (out_error) *out_error = strdup(e.what());
return -1;
}
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <cstddef>
#include <cstdint>
#ifdef __cplusplus
extern "C" {
#endif
// Compile GLSL source provided in-memory to SPIR-V-like bgfx binary
// Returns 0 on success, non-zero on failure.
int shaderc_compile_from_memory(const char* source, size_t source_len, const char* profile,
uint8_t** out_data, size_t* out_size, char** out_error);
// Variant that accepts an explicit output target language. Valid targets include:
// "spirv" (SPIR-V), "glsl" (GLSL), "msl" (Metal), "hlsl" (HLSL).
int shaderc_compile_from_memory_with_target(const char* source, size_t source_len, const char* profile,
const char* target, uint8_t** out_data, size_t* out_size, char** out_error);
// Free buffers returned by shaderc_compile_from_memory
void shaderc_free_buffer(uint8_t* data);
void shaderc_free_error(char* err);
#ifdef __cplusplus
}
#endif

View File

@@ -1,11 +1,11 @@
#include "bgfx_graphics_backend.hpp"
#include "../interfaces/i_pipeline_compiler_service.hpp"
#include <stb_image.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_properties.h>
#include <SDL3/SDL_video.h>
#include <bgfx/platform.h>
#include <shaderc/shaderc.hpp>
#include <algorithm>
#include <cstdint>
@@ -272,10 +272,12 @@ std::optional<bgfx::RendererType::Enum> RecommendFallbackRenderer(
BgfxGraphicsBackend::BgfxGraphicsBackend(std::shared_ptr<IConfigService> configService,
std::shared_ptr<IPlatformService> platformService,
std::shared_ptr<ILogger> logger)
std::shared_ptr<ILogger> logger,
std::shared_ptr<IPipelineCompilerService> pipelineCompiler)
: configService_(std::move(configService)),
platformService_(std::move(platformService)),
logger_(std::move(logger)) {
logger_(std::move(logger)),
pipelineCompiler_(std::move(pipelineCompiler)) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "BgfxGraphicsBackend",
"configService=" + std::string(configService_ ? "set" : "null") +
@@ -696,55 +698,46 @@ bgfx::ShaderHandle BgfxGraphicsBackend::CreateShader(const std::string& label,
return handle;
}
// For Vulkan/Metal/DX: Compile to SPIRV and wrap in bgfx binary format
shaderc::Compiler compiler;
shaderc::CompileOptions options;
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_1);
options.SetAutoBindUniforms(true);
// Do NOT use SetAutoMapLocations - it overrides explicit layout(location=N) declarations
// and assigns locations alphabetically by variable name, breaking the vertex layout.
// MaterialX and other shaders already specify explicit locations matching our VertexLayout.
// options.SetAutoMapLocations(true);
shaderc_shader_kind kind = isVertex ? shaderc_vertex_shader : shaderc_fragment_shader;
auto result = compiler.CompileGlslToSpv(source, kind, label.c_str(), options);
if (result.GetCompilationStatus() != shaderc_compilation_status_success) {
std::string error = result.GetErrorMessage();
if (logger_) {
logger_->Error("Shader GLSL->SPIRV compilation failed: " + label + "\n" + error);
}
throw std::runtime_error("Shader GLSL->SPIRV compilation failed: " + label + "\n" + error);
// Use PipelineCompilerService to compile shader via bgfx_tools
std::string tempInputPath = "/tmp/" + label + (isVertex ? ".vert.glsl" : ".frag.glsl");
std::string tempOutputPath = "/tmp/" + label + (isVertex ? ".vert.bin" : ".frag.bin");
// Write source to tempInputPath
{
std::ofstream ofs(tempInputPath);
ofs << source;
}
std::vector<uint32_t> spirv(result.cbegin(), result.cend());
// Wrap SPIRV with bgfx binary format: magic + hashes + uniformCount + spirvSize + spirv + nul
constexpr uint8_t kBgfxShaderVersion = 11;
constexpr uint32_t kMagicVSH = ('V') | ('S' << 8) | ('H' << 16) | (kBgfxShaderVersion << 24);
constexpr uint32_t kMagicFSH = ('F') | ('S' << 8) | ('H' << 16) | (kBgfxShaderVersion << 24);
const uint32_t magic = isVertex ? kMagicVSH : kMagicFSH;
const uint32_t inputHash = static_cast<uint32_t>(std::hash<std::string>{}(source));
const uint32_t spirvSize = static_cast<uint32_t>(spirv.size() * sizeof(uint32_t));
const uint16_t uniformCount = 0;
const uint32_t totalSize = 4 + 4 + 4 + 2 + 4 + spirvSize + 1;
const bgfx::Memory* mem = bgfx::alloc(totalSize);
uint8_t* data = mem->data;
uint32_t offset = 0;
std::memcpy(data + offset, &magic, 4); offset += 4;
std::memcpy(data + offset, &inputHash, 4); offset += 4;
std::memcpy(data + offset, &inputHash, 4); offset += 4;
std::memcpy(data + offset, &uniformCount, 2); offset += 2;
std::memcpy(data + offset, &spirvSize, 4); offset += 4;
std::memcpy(data + offset, spirv.data(), spirvSize); offset += spirvSize;
data[offset] = 0;
std::vector<std::string> args;
// Add any required args for bgfx_tools/shaderc here (e.g., profile, macros)
bool success = pipelineCompiler_ && pipelineCompiler_->Compile(tempInputPath, tempOutputPath, args);
if (!success) {
std::string error = pipelineCompiler_ ? pipelineCompiler_->GetLastError().value_or("") : "No compiler service";
if (logger_) {
logger_->Error("PipelineCompilerService failed: " + label + "\n" + error);
}
throw std::runtime_error("PipelineCompilerService failed: " + label + "\n" + error);
}
// Read compiled binary
std::ifstream ifs(tempOutputPath, std::ios::binary | std::ios::ate);
if (!ifs) {
if (logger_) {
logger_->Error("Failed to read compiled shader: " + tempOutputPath);
}
throw std::runtime_error("Failed to read compiled shader: " + tempOutputPath);
}
std::streamsize size = ifs.tellg();
ifs.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
if (!ifs.read(buffer.data(), size)) {
if (logger_) {
logger_->Error("Failed to read compiled shader data: " + tempOutputPath);
}
throw std::runtime_error("Failed to read compiled shader data: " + tempOutputPath);
}
const bgfx::Memory* mem = bgfx::copy(buffer.data(), static_cast<uint32_t>(size));
bgfx::ShaderHandle handle = bgfx::createShader(mem);
if (!bgfx::isValid(handle) && logger_) {
logger_->Error("bgfx::createShader failed for " + label +
" (renderer=" + RendererTypeName(rendererType) + ", spirvSize=" + std::to_string(spirv.size()) + " words)");
logger_->Error("bgfx::createShader failed for " + label + " (renderer=" + RendererTypeName(rendererType) + ", binSize=" + std::to_string(size) + ")");
}
return handle;
}

View File

@@ -17,7 +17,8 @@ class BgfxGraphicsBackend : public IGraphicsBackend {
public:
BgfxGraphicsBackend(std::shared_ptr<IConfigService> configService,
std::shared_ptr<IPlatformService> platformService,
std::shared_ptr<ILogger> logger);
std::shared_ptr<ILogger> logger,
std::shared_ptr<IPipelineCompilerService> pipelineCompiler);
~BgfxGraphicsBackend() override;
void Initialize(void* window, const GraphicsConfig& config) override;
@@ -111,6 +112,7 @@ private:
std::shared_ptr<IConfigService> configService_;
std::shared_ptr<IPlatformService> platformService_;
std::shared_ptr<ILogger> logger_;
std::shared_ptr<IPipelineCompilerService> pipelineCompiler_;
bgfx::VertexLayout vertexLayout_;
std::unordered_map<GraphicsPipelineHandle, std::unique_ptr<PipelineEntry>> pipelines_;
std::unordered_map<GraphicsBufferHandle, std::unique_ptr<VertexBufferEntry>> vertexBuffers_;

View File

@@ -1,18 +1,21 @@
#include "bgfx_shader_compiler.hpp"
#include <shaderc/shaderc.hpp>
#include <algorithm>
#include <cstring>
#include <numeric>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <cstdlib>
// For runtime symbol lookup
#if defined(__linux__) || defined(__APPLE__)
#include <dlfcn.h>
#endif
namespace sdl3cpp::services::impl {
namespace {
constexpr uint8_t kUniformFragmentBit = 0x10;
constexpr uint8_t kUniformMask = 0x10 | 0x20 | 0x40 | 0x80;
const char* RendererTypeName(bgfx::RendererType::Enum type) {
switch (type) {
case bgfx::RendererType::Vulkan: return "Vulkan";
@@ -26,62 +29,11 @@ const char* RendererTypeName(bgfx::RendererType::Enum type) {
}
}
uint16_t WriteUniformArray(uint8_t* data,
uint32_t& offset,
const std::vector<BgfxShaderUniform>& uniforms,
bool isFragmentShader) {
uint16_t size = 0;
const uint16_t count = static_cast<uint16_t>(uniforms.size());
std::memcpy(data + offset, &count, sizeof(count));
offset += sizeof(count);
const uint8_t fragmentBit = isFragmentShader ? kUniformFragmentBit : 0;
for (const auto& un : uniforms) {
if ((static_cast<uint8_t>(un.type) & ~kUniformMask) > bgfx::UniformType::End) {
size = std::max<uint16_t>(size, static_cast<uint16_t>(un.regIndex + un.regCount * 16));
}
const uint8_t nameSize = static_cast<uint8_t>(un.name.size());
std::memcpy(data + offset, &nameSize, sizeof(nameSize));
offset += sizeof(nameSize);
std::memcpy(data + offset, un.name.data(), nameSize);
offset += nameSize;
const uint8_t typeByte = static_cast<uint8_t>(un.type) | fragmentBit;
std::memcpy(data + offset, &typeByte, sizeof(typeByte));
offset += sizeof(typeByte);
std::memcpy(data + offset, &un.num, sizeof(un.num));
offset += sizeof(un.num);
std::memcpy(data + offset, &un.regIndex, sizeof(un.regIndex));
offset += sizeof(un.regIndex);
std::memcpy(data + offset, &un.regCount, sizeof(un.regCount));
offset += sizeof(un.regCount);
std::memcpy(data + offset, &un.texComponent, sizeof(un.texComponent));
offset += sizeof(un.texComponent);
std::memcpy(data + offset, &un.texDimension, sizeof(un.texDimension));
offset += sizeof(un.texDimension);
std::memcpy(data + offset, &un.texFormat, sizeof(un.texFormat));
offset += sizeof(un.texFormat);
}
return size;
}
uint16_t AttributeToId(bgfx::Attrib::Enum attr) {
switch (attr) {
case bgfx::Attrib::Position: return 0x0001;
case bgfx::Attrib::Color0: return 0x0005;
case bgfx::Attrib::TexCoord0: return 0x0010;
case bgfx::Attrib::Normal: return 0x0002;
case bgfx::Attrib::Tangent: return 0x0003;
case bgfx::Attrib::Bitangent: return 0x0004;
default: return 0xFFFF;
}
}
} // namespace
BgfxShaderCompiler::BgfxShaderCompiler(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {
BgfxShaderCompiler::BgfxShaderCompiler(std::shared_ptr<ILogger> logger,
std::shared_ptr<sdl3cpp::services::IPipelineCompilerService> pipelineCompiler)
: logger_(std::move(logger)), pipelineCompiler_(std::move(pipelineCompiler)) {
}
bgfx::ShaderHandle BgfxShaderCompiler::CompileShader(
@@ -90,9 +42,9 @@ bgfx::ShaderHandle BgfxShaderCompiler::CompileShader(
bool isVertex,
const std::vector<BgfxShaderUniform>& uniforms,
const std::vector<bgfx::Attrib::Enum>& attributes) const {
const bgfx::RendererType::Enum rendererType = bgfx::getRendererType();
if (logger_) {
logger_->Trace("BgfxShaderCompiler", "CompileShader",
"label=" + label +
@@ -115,63 +67,133 @@ bgfx::ShaderHandle BgfxShaderCompiler::CompileShader(
}
return handle;
}
// For Vulkan/Metal/DX: Compile GLSL to SPIRV
shaderc::Compiler compiler;
shaderc::CompileOptions options;
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_1);
options.SetAutoBindUniforms(false); // We use explicit binding=N in shaders
// Do NOT use SetAutoMapLocations - it overrides explicit layout(location=N) declarations
shaderc_shader_kind kind = isVertex ? shaderc_vertex_shader : shaderc_fragment_shader;
auto result = compiler.CompileGlslToSpv(source, kind, label.c_str(), options);
if (result.GetCompilationStatus() != shaderc_compilation_status_success) {
if (logger_) {
logger_->Error("BgfxShaderCompiler: GLSL->SPIRV compilation failed for " + label + "\n" + result.GetErrorMessage());
// Try in-memory in-process compilation first (if bgfx_tools provides C API)
std::vector<char> buffer;
bool compiledInMemory = false;
#if defined(__linux__) || defined(__APPLE__)
using shaderc_fn_t = int (*)(const char*, size_t, const char*, uint8_t**, size_t*, char**);
using shaderc_with_target_fn_t = int (*)(const char*, size_t, const char*, const char*, uint8_t**, size_t*, char**);
void* sym_with_target = dlsym(RTLD_DEFAULT, "shaderc_compile_from_memory_with_target");
shaderc_with_target_fn_t shaderc_with_target_fn = reinterpret_cast<shaderc_with_target_fn_t>(sym_with_target);
void* sym = dlsym(RTLD_DEFAULT, "shaderc_compile_from_memory");
shaderc_fn_t shaderc_fn = reinterpret_cast<shaderc_fn_t>(sym);
if (shaderc_with_target_fn || shaderc_fn) {
uint8_t* out_data = nullptr;
size_t out_size = 0;
char* out_err = nullptr;
// profile: choose based on vertex/fragment and renderer; simple heuristic
const char* profile = isVertex ? "vertex" : "fragment";
int r = -1;
if (shaderc_with_target_fn) {
// choose target based on renderer
const char* target = "spirv";
switch (rendererType) {
case bgfx::RendererType::Vulkan: target = "spirv"; break;
case bgfx::RendererType::Metal: target = "msl"; break;
case bgfx::RendererType::Direct3D11:
case bgfx::RendererType::Direct3D12: target = "hlsl"; break;
case bgfx::RendererType::OpenGL:
case bgfx::RendererType::OpenGLES: target = "glsl"; break;
default: target = "spirv"; break;
}
r = shaderc_with_target_fn(source.c_str(), source.size(), profile, target, &out_data, &out_size, &out_err);
} else if (shaderc_fn) {
r = shaderc_fn(source.c_str(), source.size(), profile, &out_data, &out_size, &out_err);
}
return BGFX_INVALID_HANDLE;
if (r == 0 && out_data && out_size > 0) {
buffer.resize(out_size);
memcpy(buffer.data(), out_data, out_size);
// free using provided free if available
// try to find free function
void* free_sym = dlsym(RTLD_DEFAULT, "shaderc_free_buffer");
if (free_sym) {
using free_fn_t = void (*)(uint8_t*);
reinterpret_cast<free_fn_t>(free_sym)(out_data);
} else {
free(out_data);
}
if (out_err) {
void* free_err_sym = dlsym(RTLD_DEFAULT, "shaderc_free_error");
if (free_err_sym) {
using free_err_fn_t = void (*)(char*);
reinterpret_cast<free_err_fn_t>(free_err_sym)(out_err);
} else {
free(out_err);
}
}
compiledInMemory = true;
if (logger_) logger_->Trace("BgfxShaderCompiler", "CompileShader", "in-memory compile succeeded for " + label);
} else {
if (out_err && logger_) {
logger_->Error(std::string("BgfxShaderCompiler: in-memory shaderc error: ") + out_err);
void* free_err_sym = dlsym(RTLD_DEFAULT, "shaderc_free_error");
if (free_err_sym) {
using free_err_fn_t = void (*)(char*);
reinterpret_cast<free_err_fn_t>(free_err_sym)(out_err);
} else {
free(out_err);
}
}
}
}
#endif
if (!compiledInMemory) {
// Fallback to temp-file + pipelineCompiler_/executable flow
std::string tempInputPath = "/tmp/" + label + (isVertex ? ".vert.glsl" : ".frag.glsl");
std::string tempOutputPath = "/tmp/" + label + (isVertex ? ".vert.bin" : ".frag.bin");
{
std::ofstream ofs(tempInputPath);
ofs << source;
}
bool ok = false;
if (pipelineCompiler_) {
ok = pipelineCompiler_->Compile(tempInputPath, tempOutputPath, {});
} else {
std::string cmd = "./src/bgfx_tools/shaderc/shaderc -f " + tempInputPath + " -o " + tempOutputPath;
if (logger_) logger_->Trace("BgfxShaderCompiler", "CompileShaderCmd", cmd);
int rc = std::system(cmd.c_str());
ok = (rc == 0);
}
if (!ok) {
if (logger_) logger_->Error("BgfxShaderCompiler: shader compilation failed for " + label);
return BGFX_INVALID_HANDLE;
}
std::ifstream ifs(tempOutputPath, std::ios::binary | std::ios::ate);
if (!ifs) {
if (logger_) logger_->Error("BgfxShaderCompiler: Failed to read compiled shader: " + tempOutputPath);
return BGFX_INVALID_HANDLE;
}
std::streamsize size = ifs.tellg();
ifs.seekg(0, std::ios::beg);
buffer.resize(size);
if (!ifs.read(buffer.data(), size)) {
if (logger_) logger_->Error("BgfxShaderCompiler: Failed to read compiled shader data");
return BGFX_INVALID_HANDLE;
}
// cleanup temp files
remove(tempInputPath.c_str());
remove(tempOutputPath.c_str());
}
std::vector<uint32_t> spirv(result.cbegin(), result.cend());
if (logger_) {
logger_->Trace("BgfxShaderCompiler", "CompileShader",
"label=" + label + " SPIRV compiled, " + std::to_string(spirv.size()) + " words");
}
// Wrap SPIRV with bgfx binary format (simple version without uniform metadata embedding)
// bgfx can extract uniform info from SPIRV reflection, so we don't need to embed it
constexpr uint8_t kBgfxShaderVersion = 11;
constexpr uint32_t kMagicVSH = ('V') | ('S' << 8) | ('H' << 16) | (kBgfxShaderVersion << 24);
constexpr uint32_t kMagicFSH = ('F') | ('S' << 8) | ('H' << 16) | (kBgfxShaderVersion << 24);
const uint32_t magic = isVertex ? kMagicVSH : kMagicFSH;
const uint32_t inputHash = static_cast<uint32_t>(std::hash<std::string>{}(source));
const uint32_t spirvSize = static_cast<uint32_t>(spirv.size() * sizeof(uint32_t));
const uint16_t uniformCount = 0; // Let bgfx extract uniforms from SPIRV
const uint32_t totalSize = 4 + 4 + 4 + 2 + 4 + spirvSize + 1;
const bgfx::Memory* mem = bgfx::alloc(totalSize);
uint8_t* data = mem->data;
uint32_t offset = 0;
std::memcpy(data + offset, &magic, 4); offset += 4;
std::memcpy(data + offset, &inputHash, 4); offset += 4;
std::memcpy(data + offset, &inputHash, 4); offset += 4; // outputHash = inputHash
std::memcpy(data + offset, &uniformCount, 2); offset += 2;
std::memcpy(data + offset, &spirvSize, 4); offset += 4;
std::memcpy(data + offset, spirv.data(), spirvSize); offset += spirvSize;
data[offset] = 0; // null terminator
uint32_t binSize = static_cast<uint32_t>(buffer.size());
const bgfx::Memory* mem = bgfx::copy(buffer.data(), binSize);
bgfx::ShaderHandle handle = bgfx::createShader(mem);
if (!bgfx::isValid(handle) && logger_) {
logger_->Error("BgfxShaderCompiler: bgfx::createShader failed for " + label +
" (spirvSize=" + std::to_string(spirv.size()) + " words)");
" (binSize=" + std::to_string(binSize) + ")");
} else if (logger_) {
logger_->Trace("BgfxShaderCompiler", "CompileShader",
"label=" + label + " shader created successfully, handle=" + std::to_string(handle.idx));
}
return handle;
}

View File

@@ -5,6 +5,7 @@
#include <memory>
#include <string>
#include <vector>
#include "../interfaces/i_pipeline_compiler_service.hpp"
namespace sdl3cpp::services::impl {
@@ -30,7 +31,8 @@ struct BgfxShaderUniform {
*/
class BgfxShaderCompiler {
public:
explicit BgfxShaderCompiler(std::shared_ptr<ILogger> logger);
explicit BgfxShaderCompiler(std::shared_ptr<ILogger> logger,
std::shared_ptr<sdl3cpp::services::IPipelineCompilerService> pipelineCompiler = nullptr);
/**
* @brief Compile GLSL source to bgfx shader handle.
@@ -53,6 +55,7 @@ public:
private:
std::shared_ptr<ILogger> logger_;
std::shared_ptr<sdl3cpp::services::IPipelineCompilerService> pipelineCompiler_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,42 @@
#include "../interfaces/i_pipeline_compiler_service.hpp"
#include "../interfaces/i_logger.hpp"
#include <cstdlib>
#include <sstream>
#include <vector>
#include <optional>
#include <string>
namespace sdl3cpp::services::impl {
class PipelineCompilerService : public IPipelineCompilerService {
public:
PipelineCompilerService(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
bool Compile(const std::string& inputPath,
const std::string& outputPath,
const std::vector<std::string>& args = {}) override {
std::ostringstream cmd;
cmd << "./src/bgfx_tools/shaderc/shaderc";
cmd << " -f " << inputPath << " -o " << outputPath;
for (const auto& arg : args) {
cmd << " " << arg;
}
logger_->Trace("PipelineCompilerService", "Compile", cmd.str());
int result = std::system(cmd.str().c_str());
if (result != 0) {
lastError_ = "bgfx_tools shaderc failed with code " + std::to_string(result);
logger_->Error(lastError_.value());
return false;
}
lastError_.reset();
return true;
}
std::optional<std::string> GetLastError() const override {
return lastError_;
}
private:
std::shared_ptr<ILogger> logger_;
std::optional<std::string> lastError_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,28 @@
#pragma once
#include <string>
#include <vector>
#include <optional>
namespace sdl3cpp::services {
/**
* @brief Interface for pipeline/shader compilation using bgfx_tools.
*/
class IPipelineCompilerService {
public:
virtual ~IPipelineCompilerService() = default;
/**
* Compile a shader or pipeline using bgfx_tools.
* @param inputPath Path to input shader source file.
* @param outputPath Path to output compiled binary.
* @param args Additional arguments for bgfx_tools (e.g., profile, macros).
* @return true if compilation succeeded, false otherwise.
*/
virtual bool Compile(const std::string& inputPath,
const std::string& outputPath,
const std::vector<std::string>& args = {}) = 0;
/**
* Get last error message (if any).
*/
virtual std::optional<std::string> GetLastError() const = 0;
};
} // namespace sdl3cpp::services