diff --git a/CMakeLists.txt b/CMakeLists.txt index 0826f9e..130763c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -331,6 +331,7 @@ add_test(NAME script_engine_tests COMMAND script_engine_tests) add_executable(bgfx_gui_service_tests tests/test_bgfx_gui_service.cpp src/services/impl/bgfx_gui_service.cpp + src/services/impl/materialx_shader_generator.cpp ) target_include_directories(bgfx_gui_service_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(bgfx_gui_service_tests PRIVATE diff --git a/src/services/impl/bgfx_gui_service.cpp b/src/services/impl/bgfx_gui_service.cpp index 0769ff9..6469221 100644 --- a/src/services/impl/bgfx_gui_service.cpp +++ b/src/services/impl/bgfx_gui_service.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace sdl3cpp::services::impl { @@ -24,6 +25,76 @@ namespace { constexpr uint64_t kGuiSamplerFlags = BGFX_SAMPLER_U_CLAMP | BGFX_SAMPLER_V_CLAMP; +constexpr uint8_t kUniformFragmentBit = 0x10; +constexpr uint8_t kUniformMask = 0 + | 0x10 + | 0x20 + | 0x40 + | 0x80; + +struct GuiShaderUniform { + std::string name; + bgfx::UniformType::Enum type = bgfx::UniformType::Count; + uint8_t num = 0; + uint16_t regIndex = 0; + uint16_t regCount = 0; + uint8_t texComponent = 0; + uint8_t texDimension = 0; + uint16_t texFormat = 0; +}; + +uint16_t WriteUniformArray(uint8_t* data, + uint32_t& offset, + const std::vector& uniforms, + bool isFragmentShader) { + uint16_t size = 0; + const uint16_t count = static_cast(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(un.type) & ~kUniformMask) > bgfx::UniformType::End) { + size = std::max(size, static_cast(un.regIndex + un.regCount * 16)); + } + + const uint8_t nameSize = static_cast(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(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 GuiAttribToId(bgfx::Attrib::Enum attr) { + switch (attr) { + case bgfx::Attrib::Position: + return 0x0001; + case bgfx::Attrib::Color0: + return 0x0005; + case bgfx::Attrib::TexCoord0: + return 0x0010; + default: + return UINT16_MAX; + } +} const char* RendererTypeName(bgfx::RendererType::Enum type) { switch (type) { @@ -107,6 +178,7 @@ BgfxGuiService::BgfxGuiService(std::shared_ptr configService, std::shared_ptr logger) : configService_(std::move(configService)), logger_(std::move(logger)), + materialxGenerator_(logger_), freeType_(std::make_unique()) { if (logger_) { logger_->Trace("BgfxGuiService", "BgfxGuiService", @@ -275,7 +347,38 @@ void BgfxGuiService::InitializeResources() { ", sampler=" + std::to_string(bgfx::isValid(sampler_))); } - program_ = CreateProgram(kGuiVertexSource, kGuiFragmentSource); + const char* vertexSource = kGuiVertexSource; + const char* fragmentSource = kGuiFragmentSource; + guiVertexSourceOverride_.clear(); + guiFragmentSourceOverride_.clear(); + + if (configService_) { + const auto& materialConfig = configService_->GetMaterialXConfig(); + if (materialConfig.enabled && materialConfig.shaderKey == "gui") { + try { + ShaderPaths generated = materialxGenerator_.Generate(materialConfig, {}); + if (!generated.vertexSource.empty() && !generated.fragmentSource.empty()) { + guiVertexSourceOverride_ = std::move(generated.vertexSource); + guiFragmentSourceOverride_ = std::move(generated.fragmentSource); + vertexSource = guiVertexSourceOverride_.c_str(); + fragmentSource = guiFragmentSourceOverride_.c_str(); + if (logger_) { + logger_->Trace("BgfxGuiService", "InitializeResources", + "Using MaterialX GUI shaders"); + } + } else if (logger_) { + logger_->Warn("BgfxGuiService::InitializeResources: MaterialX GUI shaders were empty; falling back"); + } + } catch (const std::exception& ex) { + if (logger_) { + logger_->Warn("BgfxGuiService::InitializeResources: MaterialX GUI shader generation failed: " + + std::string(ex.what())); + } + } + } + } + + program_ = CreateProgram(vertexSource, fragmentSource); const uint32_t whitePixel = 0xffffffff; whiteTexture_ = CreateTexture(reinterpret_cast(&whitePixel), 1, 1, kGuiSamplerFlags); @@ -968,15 +1071,57 @@ bgfx::ShaderHandle BgfxGuiService::CreateShader(const std::string& label, std::vector spirv(result.cbegin(), result.cend()); - // Wrap SPIRV with bgfx binary format + std::vector uniforms; + std::vector attributes; + if (isVertex) { + uniforms.push_back(GuiShaderUniform{ + "u_modelViewProj", + bgfx::UniformType::Mat4, + 1, + 0, + 4, + 0, + 0, + 0 + }); + attributes = {bgfx::Attrib::Position, bgfx::Attrib::Color0, bgfx::Attrib::TexCoord0}; + } else { + uniforms.push_back(GuiShaderUniform{ + "s_tex", + bgfx::UniformType::Sampler, + 1, + 0, + 1, + 0, + 0, + 0 + }); + } + + if (logger_) { + logger_->Trace("BgfxGuiService", "CreateShader", + "uniforms=" + std::to_string(uniforms.size()) + + ", attributes=" + std::to_string(attributes.size())); + } + + // Wrap SPIRV with bgfx binary format. 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(std::hash{}(source)); + const uint32_t varyingHash = 0x47554901; // "GUI" + 0x01 + const uint32_t inputHash = varyingHash; + const uint32_t outputHash = varyingHash; const uint32_t spirvSize = static_cast(spirv.size() * sizeof(uint32_t)); - const uint16_t uniformCount = 0; - const uint32_t totalSize = 4 + 4 + 4 + 2 + 4 + spirvSize + 1; + const uint32_t uniformDataSize = 2 + + static_cast(uniforms.size()) * (1 + 0 + 1 + 1 + 2 + 2 + 1 + 1 + 2) + + static_cast(std::accumulate( + uniforms.begin(), + uniforms.end(), + size_t{0}, + [](size_t total, const auto& un) { return total + un.name.size(); })); + const uint32_t attribDataSize = 1 + static_cast(attributes.size()) * 2; + const uint32_t totalSize = 4 + 4 + 4 + uniformDataSize + 4 + spirvSize + 1 + attribDataSize + 2; const bgfx::Memory* mem = bgfx::alloc(totalSize); uint8_t* data = mem->data; @@ -984,11 +1129,22 @@ bgfx::ShaderHandle BgfxGuiService::CreateShader(const std::string& label, 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, &outputHash, 4); offset += 4; + const uint16_t uniformSize = WriteUniformArray(data, offset, uniforms, !isVertex); std::memcpy(data + offset, &spirvSize, 4); offset += 4; std::memcpy(data + offset, spirv.data(), spirvSize); offset += spirvSize; data[offset] = 0; + offset += 1; + + const uint8_t numAttr = static_cast(attributes.size()); + std::memcpy(data + offset, &numAttr, sizeof(numAttr)); + offset += sizeof(numAttr); + for (auto attr : attributes) { + const uint16_t attrId = GuiAttribToId(attr); + std::memcpy(data + offset, &attrId, sizeof(attrId)); + offset += sizeof(attrId); + } + std::memcpy(data + offset, &uniformSize, sizeof(uniformSize)); bgfx::ShaderHandle handle = bgfx::createShader(mem); if (!bgfx::isValid(handle) && logger_) { diff --git a/src/services/impl/bgfx_gui_service.hpp b/src/services/impl/bgfx_gui_service.hpp index 1d03922..83cbc24 100644 --- a/src/services/impl/bgfx_gui_service.hpp +++ b/src/services/impl/bgfx_gui_service.hpp @@ -7,6 +7,8 @@ #include +#include "materialx_shader_generator.hpp" + #include #include #include @@ -144,6 +146,7 @@ private: std::shared_ptr configService_; std::shared_ptr logger_; + MaterialXShaderGenerator materialxGenerator_; std::unique_ptr freeType_; std::unordered_map textCache_; @@ -154,6 +157,8 @@ private: bgfx::UniformHandle modelViewProjUniform_ = BGFX_INVALID_HANDLE; bgfx::TextureHandle whiteTexture_ = BGFX_INVALID_HANDLE; bgfx::VertexLayout layout_; + std::string guiVertexSourceOverride_; + std::string guiFragmentSourceOverride_; std::vector scissorStack_; std::array viewProjection_{}; diff --git a/tests/test_bgfx_gui_service.cpp b/tests/test_bgfx_gui_service.cpp index c0cc98c..5dca7c4 100644 --- a/tests/test_bgfx_gui_service.cpp +++ b/tests/test_bgfx_gui_service.cpp @@ -118,6 +118,15 @@ public: return false; } + bool HasSubstring(const std::string& fragment) const { + for (const auto& entry : entries_) { + if (entry.second.find(fragment) != std::string::npos) { + return true; + } + } + return false; + } + private: sdl3cpp::services::LogLevel level_ = sdl3cpp::services::LogLevel::TRACE; bool consoleEnabled_ = false; @@ -145,7 +154,19 @@ public: guiFontConfig_.useFreeType = false; } + void EnableMaterialXGuiShader() { + materialXConfig_.enabled = true; + materialXConfig_.useConstantColor = true; + materialXConfig_.shaderKey = "gui"; + materialXConfig_.libraryPath = ResolveMaterialXLibraryPath(); + } + private: + static std::filesystem::path ResolveMaterialXLibraryPath() { + auto repoRoot = std::filesystem::path(__FILE__).parent_path().parent_path(); + return repoRoot / "MaterialX" / "libraries"; + } + sdl3cpp::services::InputBindings inputBindings_{}; sdl3cpp::services::MouseGrabConfig mouseGrabConfig_{}; sdl3cpp::services::BgfxConfig bgfxConfig_{}; @@ -181,12 +202,15 @@ int main() { auto logger = std::make_shared(); auto configService = std::make_shared(); configService->DisableFreeType(); + configService->EnableMaterialXGuiShader(); sdl3cpp::services::impl::BgfxGuiService service(configService, logger); service.PrepareFrame({}, 1, 1); Assert(service.IsProgramReady(), "GUI shader program should link", failures); Assert(service.IsWhiteTextureReady(), "white texture should be created", failures); + Assert(logger->HasSubstring("Using MaterialX GUI shaders"), + "expected MaterialX GUI shader path", failures); if (!service.IsProgramReady() && !logger->HasErrorSubstring("bgfx::createProgram failed to link shaders")) {