feat: Enhance GUI rendering and configuration management

- Added support for FreeType font rendering in GuiRenderer, including glyph loading and text rendering capabilities.
- Introduced GuiFontConfig structure to manage font settings through configuration.
- Updated JsonConfigService to parse and provide GUI font settings from JSON configuration.
- Implemented MaterialX shader generation capabilities with a new MaterialXShaderGenerator class.
- Enhanced ShaderScriptService to utilize MaterialX for shader generation based on configuration.
- Added NullGuiService as a placeholder implementation for GUI service.
- Extended IConfigService interface to include methods for retrieving graphics backend, MaterialX, and GUI font configurations.
- Updated RuntimeConfig structure to include graphics backend and MaterialX configurations.
- Added tests to ensure proper integration of new configuration settings and shader generation functionality.
This commit is contained in:
2026-01-06 14:38:42 +00:00
parent 1549fa3675
commit 5e18571856
19 changed files with 1464 additions and 115 deletions

View File

@@ -136,8 +136,9 @@ find_package(lua CONFIG REQUIRED)
find_package(CLI11 CONFIG REQUIRED)
find_package(RapidJSON CONFIG REQUIRED)
find_package(EnTT CONFIG REQUIRED)
find_package(bgfx CONFIG QUIET)
find_package(MaterialX CONFIG QUIET)
find_package(bgfx CONFIG REQUIRED)
find_package(MaterialX CONFIG REQUIRED)
find_package(Freetype CONFIG REQUIRED)
find_package(assimp CONFIG REQUIRED)
find_package(Bullet CONFIG REQUIRED)
find_package(Vorbis CONFIG REQUIRED)
@@ -159,10 +160,12 @@ set(SDL3CPP_MATERIALX_LIBS)
if(TARGET MaterialX::MaterialX)
list(APPEND SDL3CPP_MATERIALX_LIBS MaterialX::MaterialX)
else()
foreach(candidate IN ITEMS
MaterialX::MaterialXCore
MaterialX::MaterialXFormat
MaterialX::MaterialXGenShader)
foreach(candidate IN ITEMS
MaterialX::MaterialXCore
MaterialX::MaterialXFormat
MaterialX::MaterialXGenShader
MaterialX::MaterialXGenGlsl
MaterialX::MaterialXRender)
if(TARGET ${candidate})
list(APPEND SDL3CPP_MATERIALX_LIBS ${candidate})
endif()
@@ -171,6 +174,13 @@ endif()
if(SDL3CPP_MATERIALX_LIBS)
list(APPEND SDL3CPP_RENDER_STACK_LIBS ${SDL3CPP_MATERIALX_LIBS})
endif()
set(SDL3CPP_FREETYPE_LIBS)
if(TARGET Freetype::Freetype)
list(APPEND SDL3CPP_FREETYPE_LIBS Freetype::Freetype)
elseif(TARGET freetype::freetype)
list(APPEND SDL3CPP_FREETYPE_LIBS freetype::freetype)
endif()
endif()
if(BUILD_SDL3_APP)
@@ -188,6 +198,7 @@ if(BUILD_SDL3_APP)
src/services/impl/lua_helpers.cpp
src/services/impl/scene_script_service.cpp
src/services/impl/shader_script_service.cpp
src/services/impl/materialx_shader_generator.cpp
src/services/impl/render_graph_script_service.cpp
src/services/impl/gui_script_service.cpp
src/services/impl/audio_command_service.cpp
@@ -207,10 +218,12 @@ if(BUILD_SDL3_APP)
src/services/impl/render_coordinator_service.cpp
src/services/impl/gui_renderer_service.cpp
src/services/impl/vulkan_gui_service.cpp
src/services/impl/null_gui_service.cpp
src/services/impl/bullet_physics_service.cpp
src/services/impl/scene_service.cpp
src/services/impl/graphics_service.cpp
$<$<NOT:$<BOOL:${ENABLE_VITA}>>:src/services/impl/vulkan_graphics_backend.cpp>
$<$<NOT:$<BOOL:${ENABLE_VITA}>>:src/services/impl/bgfx_graphics_backend.cpp>
$<$<BOOL:${ENABLE_VITA}>:src/services/impl/gxm_graphics_backend.cpp>
src/app/service_based_app.cpp
src/services/impl/gui_renderer.cpp
@@ -223,6 +236,7 @@ if(BUILD_SDL3_APP)
CLI11::CLI11
rapidjson
${SDL3CPP_RENDER_STACK_LIBS}
${SDL3CPP_FREETYPE_LIBS}
assimp::assimp
Bullet::Bullet
glm::glm
@@ -285,11 +299,13 @@ add_executable(script_engine_tests
src/services/impl/lua_helpers.cpp
src/services/impl/scene_script_service.cpp
src/services/impl/shader_script_service.cpp
src/services/impl/materialx_shader_generator.cpp
)
target_include_directories(script_engine_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(script_engine_tests PRIVATE
${SDL_TARGET}
lua::lua
${SDL3CPP_RENDER_STACK_LIBS}
assimp::assimp
Bullet::Bullet
glm::glm

View File

@@ -20,6 +20,7 @@
#include "services/impl/render_command_service.hpp"
#include "services/impl/graphics_service.hpp"
#include "services/impl/vulkan_graphics_backend.hpp"
#include "services/impl/bgfx_graphics_backend.hpp"
#include "services/impl/script_engine_service.hpp"
#include "services/impl/scene_script_service.hpp"
#include "services/impl/shader_script_service.hpp"
@@ -32,6 +33,7 @@
#include "services/impl/gui_renderer_service.hpp"
#include "services/impl/sdl_audio_service.hpp"
#include "services/impl/vulkan_gui_service.hpp"
#include "services/impl/null_gui_service.hpp"
#include "services/impl/bullet_physics_service.hpp"
#include "services/impl/crash_recovery_service.hpp"
#include "services/impl/logger_service.hpp"
@@ -141,14 +143,22 @@ void ServiceBasedApp::Run() {
// Initialize GUI service after graphics
auto guiService = registry_.GetService<services::IGuiService>();
auto vulkanDeviceService = registry_.GetService<services::IVulkanDeviceService>();
auto swapchainService = registry_.GetService<services::ISwapchainService>();
if (guiService && vulkanDeviceService && swapchainService) {
guiService->Initialize(vulkanDeviceService->GetDevice(),
vulkanDeviceService->GetPhysicalDevice(),
swapchainService->GetSwapchainImageFormat(),
swapchainService->GetRenderPass(),
runtimeConfig_.scriptPath.parent_path());
bool useBgfx = false;
if (configService) {
useBgfx = configService->GetGraphicsBackendConfig().backend == services::GraphicsBackendType::Bgfx;
}
if (!useBgfx &&
registry_.HasService<services::IVulkanDeviceService>() &&
registry_.HasService<services::ISwapchainService>()) {
auto vulkanDeviceService = registry_.GetService<services::IVulkanDeviceService>();
auto swapchainService = registry_.GetService<services::ISwapchainService>();
if (guiService && vulkanDeviceService && swapchainService) {
guiService->Initialize(vulkanDeviceService->GetDevice(),
vulkanDeviceService->GetPhysicalDevice(),
swapchainService->GetSwapchainImageFormat(),
swapchainService->GetRenderPass(),
runtimeConfig_.scriptPath.parent_path());
}
}
// Run the main application loop with crash recovery
@@ -231,6 +241,11 @@ void ServiceBasedApp::RegisterServices() {
// Configuration service
registry_.RegisterService<services::IConfigService, services::impl::JsonConfigService>(
registry_.GetService<services::ILogger>(), runtimeConfig_);
auto configService = registry_.GetService<services::IConfigService>();
bool useBgfx = false;
if (configService) {
useBgfx = configService->GetGraphicsBackendConfig().backend == services::GraphicsBackendType::Bgfx;
}
// ECS service (entt registry)
registry_.RegisterService<services::IEcsService, services::impl::EcsService>(
@@ -281,6 +296,7 @@ void ServiceBasedApp::RegisterServices() {
registry_.GetService<services::ILogger>());
registry_.RegisterService<services::IShaderScriptService, services::impl::ShaderScriptService>(
registry_.GetService<services::IScriptEngineService>(),
registry_.GetService<services::IConfigService>(),
registry_.GetService<services::ILogger>());
registry_.RegisterService<services::IRenderGraphScriptService, services::impl::RenderGraphScriptService>(
registry_.GetService<services::IScriptEngineService>(),
@@ -299,53 +315,63 @@ void ServiceBasedApp::RegisterServices() {
inputService->SetGuiScriptService(guiScriptService.get());
}
// Vulkan device service
registry_.RegisterService<services::IVulkanDeviceService, services::impl::VulkanDeviceService>(
registry_.GetService<services::ILogger>());
std::shared_ptr<services::IGraphicsBackend> graphicsBackend;
if (!useBgfx) {
// Vulkan device service
registry_.RegisterService<services::IVulkanDeviceService, services::impl::VulkanDeviceService>(
registry_.GetService<services::ILogger>());
// Swapchain service
registry_.RegisterService<services::ISwapchainService, services::impl::SwapchainService>(
registry_.GetService<services::IVulkanDeviceService>(),
registry_.GetService<events::IEventBus>(),
registry_.GetService<services::ILogger>());
// Swapchain service
registry_.RegisterService<services::ISwapchainService, services::impl::SwapchainService>(
registry_.GetService<services::IVulkanDeviceService>(),
registry_.GetService<events::IEventBus>(),
registry_.GetService<services::ILogger>());
// Pipeline service
registry_.RegisterService<services::IPipelineService, services::impl::PipelineService>(
registry_.GetService<services::IVulkanDeviceService>(),
registry_.GetService<services::ILogger>());
// Pipeline service
registry_.RegisterService<services::IPipelineService, services::impl::PipelineService>(
registry_.GetService<services::IVulkanDeviceService>(),
registry_.GetService<services::ILogger>());
// Buffer service
registry_.RegisterService<services::IBufferService, services::impl::BufferService>(
registry_.GetService<services::IVulkanDeviceService>(),
registry_.GetService<services::ILogger>());
// Buffer service
registry_.RegisterService<services::IBufferService, services::impl::BufferService>(
registry_.GetService<services::IVulkanDeviceService>(),
registry_.GetService<services::ILogger>());
// GUI renderer service (needed by render command service and GUI service)
registry_.RegisterService<services::IGuiRendererService, services::impl::GuiRendererService>(
registry_.GetService<services::ILogger>(),
registry_.GetService<services::IBufferService>());
logger_->Trace("ServiceBasedApp", "RegisterServices",
"Registered GUI renderer service before render command service");
// GUI renderer service (needed by render command service and GUI service)
registry_.RegisterService<services::IGuiRendererService, services::impl::GuiRendererService>(
registry_.GetService<services::ILogger>(),
registry_.GetService<services::IBufferService>(),
registry_.GetService<services::IConfigService>());
logger_->Trace("ServiceBasedApp", "RegisterServices",
"Registered GUI renderer service before render command service");
// Render command service
registry_.RegisterService<services::IRenderCommandService, services::impl::RenderCommandService>(
registry_.GetService<services::IVulkanDeviceService>(),
registry_.GetService<services::ISwapchainService>(),
registry_.GetService<services::IPipelineService>(),
registry_.GetService<services::IBufferService>(),
registry_.GetService<services::IGuiRendererService>(),
std::static_pointer_cast<services::impl::JsonConfigService>(registry_.GetService<services::IConfigService>()),
registry_.GetService<services::ILogger>());
// Render command service
registry_.RegisterService<services::IRenderCommandService, services::impl::RenderCommandService>(
registry_.GetService<services::IVulkanDeviceService>(),
registry_.GetService<services::ISwapchainService>(),
registry_.GetService<services::IPipelineService>(),
registry_.GetService<services::IBufferService>(),
registry_.GetService<services::IGuiRendererService>(),
std::static_pointer_cast<services::impl::JsonConfigService>(registry_.GetService<services::IConfigService>()),
registry_.GetService<services::ILogger>());
// Graphics service (facade)
registry_.RegisterService<services::IGraphicsService, services::impl::GraphicsService>(
registry_.GetService<services::ILogger>(),
std::make_shared<services::impl::VulkanGraphicsBackend>(
graphicsBackend = std::make_shared<services::impl::VulkanGraphicsBackend>(
registry_.GetService<services::IVulkanDeviceService>(),
registry_.GetService<services::ISwapchainService>(),
registry_.GetService<services::IRenderCommandService>(),
registry_.GetService<services::IPipelineService>(),
registry_.GetService<services::IBufferService>(),
registry_.GetService<services::ILogger>()),
registry_.GetService<services::ILogger>());
} else {
graphicsBackend = std::make_shared<services::impl::BgfxGraphicsBackend>(
registry_.GetService<services::IConfigService>(),
registry_.GetService<services::ILogger>());
}
// Graphics service (facade)
registry_.RegisterService<services::IGraphicsService, services::impl::GraphicsService>(
registry_.GetService<services::ILogger>(),
graphicsBackend,
registry_.GetService<services::IWindowService>());
// Scene service
@@ -355,9 +381,14 @@ void ServiceBasedApp::RegisterServices() {
registry_.GetService<services::ILogger>());
// GUI service
registry_.RegisterService<services::IGuiService, services::impl::VulkanGuiService>(
registry_.GetService<services::ILogger>(),
registry_.GetService<services::IGuiRendererService>());
if (!useBgfx) {
registry_.RegisterService<services::IGuiService, services::impl::VulkanGuiService>(
registry_.GetService<services::ILogger>(),
registry_.GetService<services::IGuiRendererService>());
} else {
registry_.RegisterService<services::IGuiService, services::impl::NullGuiService>(
registry_.GetService<services::ILogger>());
}
// Physics service
registry_.RegisterService<services::IPhysicsService, services::impl::BulletPhysicsService>(

View File

@@ -0,0 +1,461 @@
#include "bgfx_graphics_backend.hpp"
#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>
#include <cctype>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <stdexcept>
namespace sdl3cpp::services::impl {
namespace {
std::string ToLower(std::string value) {
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return value;
}
bgfx::RendererType::Enum RendererFromString(const std::string& value) {
const std::string lower = ToLower(value);
if (lower == "vulkan") {
return bgfx::RendererType::Vulkan;
}
if (lower == "auto") {
return bgfx::RendererType::Count;
}
return bgfx::RendererType::Vulkan;
}
} // namespace
BgfxGraphicsBackend::BgfxGraphicsBackend(std::shared_ptr<IConfigService> configService,
std::shared_ptr<ILogger> logger)
: configService_(std::move(configService)),
logger_(std::move(logger)) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "BgfxGraphicsBackend",
"configService=" + std::string(configService_ ? "set" : "null"));
}
vertexLayout_.begin()
.add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
.add(bgfx::Attrib::Color0, 3, bgfx::AttribType::Float)
.end();
}
BgfxGraphicsBackend::~BgfxGraphicsBackend() {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "~BgfxGraphicsBackend");
}
if (initialized_) {
Shutdown();
}
}
void BgfxGraphicsBackend::SetupPlatformData(void* window) {
bgfx::PlatformData pd{};
SDL_Window* sdlWindow = static_cast<SDL_Window*>(window);
if (!sdlWindow) {
bgfx::setPlatformData(pd);
return;
}
SDL_PropertiesID props = SDL_GetWindowProperties(sdlWindow);
#if defined(_WIN32)
pd.nwh = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
#elif defined(__APPLE__)
pd.nwh = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);
#elif defined(__linux__)
void* wlDisplay = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);
void* wlSurface = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);
if (wlDisplay && wlSurface) {
pd.ndt = wlDisplay;
pd.nwh = wlSurface;
} else {
void* x11Display = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);
Sint64 x11Window = SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
if (x11Display && x11Window != 0) {
pd.ndt = x11Display;
pd.nwh = reinterpret_cast<void*>(static_cast<uintptr_t>(x11Window));
}
}
#else
pd.nwh = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
#endif
bgfx::setPlatformData(pd);
}
bgfx::RendererType::Enum BgfxGraphicsBackend::ResolveRendererType() const {
if (!configService_) {
return bgfx::RendererType::Vulkan;
}
const auto& config = configService_->GetGraphicsBackendConfig();
bgfx::RendererType::Enum renderer = RendererFromString(config.bgfxRenderer);
if (renderer != bgfx::RendererType::Vulkan) {
if (logger_) {
logger_->Warn("BgfxGraphicsBackend: Forcing bgfx renderer to Vulkan");
}
renderer = bgfx::RendererType::Vulkan;
}
return renderer;
}
void BgfxGraphicsBackend::Initialize(void* window, const GraphicsConfig& config) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "Initialize");
}
if (initialized_) {
return;
}
SDL_Window* sdlWindow = static_cast<SDL_Window*>(window);
int width = 0;
int height = 0;
if (sdlWindow) {
SDL_GetWindowSizeInPixels(sdlWindow, &width, &height);
}
viewportWidth_ = static_cast<uint32_t>(std::max(1, width));
viewportHeight_ = static_cast<uint32_t>(std::max(1, height));
SetupPlatformData(window);
bgfx::Init init{};
init.type = ResolveRendererType();
init.resolution.width = viewportWidth_;
init.resolution.height = viewportHeight_;
init.resolution.reset = BGFX_RESET_VSYNC;
if (!bgfx::init(init)) {
throw std::runtime_error("Failed to initialize bgfx");
}
bgfx::setViewClear(viewId_, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x1f1f1fff, 1.0f, 0);
bgfx::setDebug(BGFX_DEBUG_TEXT);
initialized_ = true;
}
void BgfxGraphicsBackend::Shutdown() {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "Shutdown");
}
if (!initialized_) {
return;
}
DestroyPipelines();
DestroyBuffers();
bgfx::shutdown();
initialized_ = false;
}
void BgfxGraphicsBackend::RecreateSwapchain(uint32_t width, uint32_t height) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "RecreateSwapchain",
"width=" + std::to_string(width) +
", height=" + std::to_string(height));
}
if (!initialized_) {
return;
}
if (width == 0 || height == 0) {
return;
}
viewportWidth_ = width;
viewportHeight_ = height;
bgfx::reset(viewportWidth_, viewportHeight_, BGFX_RESET_VSYNC);
}
void BgfxGraphicsBackend::WaitIdle() {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "WaitIdle");
}
}
GraphicsDeviceHandle BgfxGraphicsBackend::CreateDevice() {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "CreateDevice");
}
return reinterpret_cast<GraphicsDeviceHandle>(1);
}
void BgfxGraphicsBackend::DestroyDevice(GraphicsDeviceHandle device) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "DestroyDevice");
}
}
std::vector<uint8_t> BgfxGraphicsBackend::ReadShaderSource(const std::string& path,
const std::string& source) const {
if (!source.empty()) {
return std::vector<uint8_t>(source.begin(), source.end());
}
if (path.empty()) {
throw std::runtime_error("Shader path and source are empty");
}
std::filesystem::path shaderPath(path);
if (shaderPath.extension() == ".spv") {
shaderPath.replace_extension();
}
if (!std::filesystem::exists(shaderPath)) {
throw std::runtime_error("Shader file not found: " + shaderPath.string());
}
std::ifstream sourceFile(shaderPath);
if (!sourceFile) {
throw std::runtime_error("Failed to open shader source: " + shaderPath.string());
}
return std::vector<uint8_t>((std::istreambuf_iterator<char>(sourceFile)),
std::istreambuf_iterator<char>());
}
bgfx::ShaderHandle BgfxGraphicsBackend::CreateShader(const std::string& label,
const std::string& source,
bool isVertex) const {
shaderc::Compiler compiler;
shaderc::CompileOptions options;
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2);
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("Bgfx shader compilation failed: " + label + "\n" + error);
}
throw std::runtime_error("Bgfx shader compilation failed: " + label + "\n" + error);
}
std::vector<uint32_t> spirv(result.cbegin(), result.cend());
const bgfx::Memory* mem = bgfx::copy(spirv.data(),
static_cast<uint32_t>(spirv.size() * sizeof(uint32_t)));
return bgfx::createShader(mem);
}
GraphicsPipelineHandle BgfxGraphicsBackend::CreatePipeline(GraphicsDeviceHandle device,
const std::string& shaderKey,
const ShaderPaths& shaderPaths) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "CreatePipeline", "shaderKey=" + shaderKey);
}
std::vector<uint8_t> vertexBytes = ReadShaderSource(shaderPaths.vertex, shaderPaths.vertexSource);
std::vector<uint8_t> fragmentBytes = ReadShaderSource(shaderPaths.fragment, shaderPaths.fragmentSource);
std::string vertexSource(vertexBytes.begin(), vertexBytes.end());
std::string fragmentSource(fragmentBytes.begin(), fragmentBytes.end());
bgfx::ShaderHandle vs = CreateShader(shaderKey + ":vertex", vertexSource, true);
bgfx::ShaderHandle fs = CreateShader(shaderKey + ":fragment", fragmentSource, false);
bgfx::ProgramHandle program = bgfx::createProgram(vs, fs, true);
auto entry = std::make_unique<PipelineEntry>();
entry->program = program;
GraphicsPipelineHandle handle = reinterpret_cast<GraphicsPipelineHandle>(entry.get());
pipelines_.emplace(handle, std::move(entry));
return handle;
}
void BgfxGraphicsBackend::DestroyPipeline(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "DestroyPipeline");
}
auto it = pipelines_.find(pipeline);
if (it == pipelines_.end()) {
return;
}
if (bgfx::isValid(it->second->program)) {
bgfx::destroy(it->second->program);
}
pipelines_.erase(it);
}
GraphicsBufferHandle BgfxGraphicsBackend::CreateVertexBuffer(GraphicsDeviceHandle device,
const std::vector<uint8_t>& data) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "CreateVertexBuffer",
"data.size=" + std::to_string(data.size()));
}
if (data.empty() || data.size() % sizeof(core::Vertex) != 0) {
throw std::runtime_error("Vertex data invalid for bgfx");
}
uint32_t vertexCount = static_cast<uint32_t>(data.size() / sizeof(core::Vertex));
const bgfx::Memory* mem = bgfx::copy(data.data(), static_cast<uint32_t>(data.size()));
bgfx::VertexBufferHandle buffer = bgfx::createVertexBuffer(mem, vertexLayout_);
auto entry = std::make_unique<VertexBufferEntry>();
entry->handle = buffer;
entry->vertexCount = vertexCount;
GraphicsBufferHandle handle = reinterpret_cast<GraphicsBufferHandle>(entry.get());
vertexBuffers_.emplace(handle, std::move(entry));
return handle;
}
GraphicsBufferHandle BgfxGraphicsBackend::CreateIndexBuffer(GraphicsDeviceHandle device,
const std::vector<uint8_t>& data) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "CreateIndexBuffer",
"data.size=" + std::to_string(data.size()));
}
if (data.empty() || data.size() % sizeof(uint16_t) != 0) {
throw std::runtime_error("Index data invalid for bgfx");
}
uint32_t indexCount = static_cast<uint32_t>(data.size() / sizeof(uint16_t));
const bgfx::Memory* mem = bgfx::copy(data.data(), static_cast<uint32_t>(data.size()));
bgfx::IndexBufferHandle buffer = bgfx::createIndexBuffer(mem);
auto entry = std::make_unique<IndexBufferEntry>();
entry->handle = buffer;
entry->indexCount = indexCount;
GraphicsBufferHandle handle = reinterpret_cast<GraphicsBufferHandle>(entry.get());
indexBuffers_.emplace(handle, std::move(entry));
return handle;
}
void BgfxGraphicsBackend::DestroyBuffer(GraphicsDeviceHandle device, GraphicsBufferHandle buffer) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "DestroyBuffer");
}
auto vIt = vertexBuffers_.find(buffer);
if (vIt != vertexBuffers_.end()) {
if (bgfx::isValid(vIt->second->handle)) {
bgfx::destroy(vIt->second->handle);
}
vertexBuffers_.erase(vIt);
return;
}
auto iIt = indexBuffers_.find(buffer);
if (iIt != indexBuffers_.end()) {
if (bgfx::isValid(iIt->second->handle)) {
bgfx::destroy(iIt->second->handle);
}
indexBuffers_.erase(iIt);
}
}
bool BgfxGraphicsBackend::BeginFrame(GraphicsDeviceHandle device) {
if (!initialized_) {
return false;
}
const float identity[16] = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
bgfx::setViewRect(viewId_, 0, 0, viewportWidth_, viewportHeight_);
bgfx::setViewTransform(viewId_, viewProj_.data(), identity);
bgfx::touch(viewId_);
return true;
}
bool BgfxGraphicsBackend::EndFrame(GraphicsDeviceHandle device) {
if (!initialized_) {
return false;
}
bgfx::frame();
return true;
}
void BgfxGraphicsBackend::SetViewProjection(const std::array<float, 16>& viewProj) {
viewProj_ = viewProj;
}
void BgfxGraphicsBackend::SetRenderGraphDefinition(const RenderGraphDefinition& definition) {
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "SetRenderGraphDefinition",
"passes=" + std::to_string(definition.passes.size()));
}
}
void BgfxGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline,
GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer,
uint32_t indexOffset, uint32_t indexCount, int32_t vertexOffset,
const std::array<float, 16>& modelMatrix) {
auto pipelineIt = pipelines_.find(pipeline);
if (pipelineIt == pipelines_.end()) {
if (logger_) {
logger_->Error("BgfxGraphicsBackend::Draw: Pipeline not found");
}
return;
}
auto vertexIt = vertexBuffers_.find(vertexBuffer);
auto indexIt = indexBuffers_.find(indexBuffer);
if (vertexIt == vertexBuffers_.end() || indexIt == indexBuffers_.end()) {
if (logger_) {
logger_->Error("BgfxGraphicsBackend::Draw: Buffer handles not found");
}
return;
}
const auto& vb = vertexIt->second;
const auto& ib = indexIt->second;
uint32_t startVertex = static_cast<uint32_t>(std::max(0, vertexOffset));
uint32_t availableVertices = vb->vertexCount > startVertex
? vb->vertexCount - startVertex
: 0;
if (availableVertices == 0) {
return;
}
bgfx::setTransform(modelMatrix.data());
bgfx::setVertexBuffer(0, vb->handle, startVertex, availableVertices);
bgfx::setIndexBuffer(ib->handle, indexOffset, indexCount);
bgfx::setState(BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A | BGFX_STATE_WRITE_Z |
BGFX_STATE_DEPTH_TEST_LESS | BGFX_STATE_CULL_CW | BGFX_STATE_MSAA);
bgfx::submit(viewId_, pipelineIt->second->program);
}
GraphicsDeviceHandle BgfxGraphicsBackend::GetPhysicalDevice() const {
return nullptr;
}
std::pair<uint32_t, uint32_t> BgfxGraphicsBackend::GetSwapchainExtent() const {
return {viewportWidth_, viewportHeight_};
}
uint32_t BgfxGraphicsBackend::GetSwapchainFormat() const {
return 0;
}
void* BgfxGraphicsBackend::GetCurrentCommandBuffer() const {
return nullptr;
}
void* BgfxGraphicsBackend::GetGraphicsQueue() const {
return nullptr;
}
void BgfxGraphicsBackend::DestroyPipelines() {
for (auto& [handle, entry] : pipelines_) {
if (bgfx::isValid(entry->program)) {
bgfx::destroy(entry->program);
}
}
pipelines_.clear();
}
void BgfxGraphicsBackend::DestroyBuffers() {
for (auto& [handle, entry] : vertexBuffers_) {
if (bgfx::isValid(entry->handle)) {
bgfx::destroy(entry->handle);
}
}
vertexBuffers_.clear();
for (auto& [handle, entry] : indexBuffers_) {
if (bgfx::isValid(entry->handle)) {
bgfx::destroy(entry->handle);
}
}
indexBuffers_.clear();
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,88 @@
#pragma once
#include "../interfaces/i_config_service.hpp"
#include "../interfaces/i_graphics_backend.hpp"
#include "../interfaces/i_logger.hpp"
#include "../../core/vertex.hpp"
#include <bgfx/bgfx.h>
#include <array>
#include <memory>
#include <unordered_map>
#include <vector>
namespace sdl3cpp::services::impl {
class BgfxGraphicsBackend : public IGraphicsBackend {
public:
BgfxGraphicsBackend(std::shared_ptr<IConfigService> configService,
std::shared_ptr<ILogger> logger);
~BgfxGraphicsBackend() override;
void Initialize(void* window, const GraphicsConfig& config) override;
void Shutdown() override;
void RecreateSwapchain(uint32_t width, uint32_t height) override;
void WaitIdle() override;
GraphicsDeviceHandle CreateDevice() override;
void DestroyDevice(GraphicsDeviceHandle device) override;
GraphicsPipelineHandle CreatePipeline(GraphicsDeviceHandle device,
const std::string& shaderKey,
const ShaderPaths& shaderPaths) override;
void DestroyPipeline(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline) override;
GraphicsBufferHandle CreateVertexBuffer(GraphicsDeviceHandle device,
const std::vector<uint8_t>& data) override;
GraphicsBufferHandle CreateIndexBuffer(GraphicsDeviceHandle device,
const std::vector<uint8_t>& data) override;
void DestroyBuffer(GraphicsDeviceHandle device, GraphicsBufferHandle buffer) override;
bool BeginFrame(GraphicsDeviceHandle device) override;
bool EndFrame(GraphicsDeviceHandle device) override;
void SetViewProjection(const std::array<float, 16>& viewProj) override;
void SetRenderGraphDefinition(const RenderGraphDefinition& definition) override;
void Draw(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline,
GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer,
uint32_t indexOffset, uint32_t indexCount, int32_t vertexOffset,
const std::array<float, 16>& modelMatrix) override;
GraphicsDeviceHandle GetPhysicalDevice() const override;
std::pair<uint32_t, uint32_t> GetSwapchainExtent() const override;
uint32_t GetSwapchainFormat() const override;
void* GetCurrentCommandBuffer() const override;
void* GetGraphicsQueue() const override;
private:
struct PipelineEntry {
bgfx::ProgramHandle program = BGFX_INVALID_HANDLE;
};
struct VertexBufferEntry {
bgfx::VertexBufferHandle handle = BGFX_INVALID_HANDLE;
uint32_t vertexCount = 0;
};
struct IndexBufferEntry {
bgfx::IndexBufferHandle handle = BGFX_INVALID_HANDLE;
uint32_t indexCount = 0;
};
void SetupPlatformData(void* window);
bgfx::RendererType::Enum ResolveRendererType() const;
std::vector<uint8_t> ReadShaderSource(const std::string& path,
const std::string& source) const;
bgfx::ShaderHandle CreateShader(const std::string& label,
const std::string& source,
bool isVertex) const;
void DestroyPipelines();
void DestroyBuffers();
std::shared_ptr<IConfigService> configService_;
std::shared_ptr<ILogger> logger_;
bgfx::VertexLayout vertexLayout_;
std::unordered_map<GraphicsPipelineHandle, std::unique_ptr<PipelineEntry>> pipelines_;
std::unordered_map<GraphicsBufferHandle, std::unique_ptr<VertexBufferEntry>> vertexBuffers_;
std::unordered_map<GraphicsBufferHandle, std::unique_ptr<IndexBufferEntry>> indexBuffers_;
std::array<float, 16> viewProj_{};
uint32_t viewportWidth_ = 0;
uint32_t viewportHeight_ = 0;
bool initialized_ = false;
bgfx::ViewId viewId_ = 0;
};
} // namespace sdl3cpp::services::impl

View File

@@ -1,5 +1,8 @@
#include "gui_renderer.hpp"
#include <ft2build.h>
#include FT_FREETYPE_H
#include <algorithm>
#include <array>
#include <cmath>
@@ -407,6 +410,7 @@ void main() {
GuiRenderer::GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFormat swapchainFormat,
VkRenderPass renderPass, const std::filesystem::path& scriptDirectory,
const GuiFontConfig& fontConfig,
std::shared_ptr<IBufferService> bufferService,
std::shared_ptr<ILogger> logger)
: device_(device),
@@ -414,6 +418,7 @@ GuiRenderer::GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFor
swapchainFormat_(swapchainFormat),
renderPass_(renderPass),
scriptDirectory_(scriptDirectory),
fontConfig_(fontConfig),
bufferService_(std::move(bufferService)),
logger_(std::move(logger)) {
}
@@ -421,6 +426,16 @@ GuiRenderer::GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFor
GuiRenderer::~GuiRenderer() {
CleanupBuffers();
CleanupPipeline();
if (freetypeReady_) {
FT_Face face = reinterpret_cast<FT_Face>(ftFace_);
FT_Library library = reinterpret_cast<FT_Library>(ftLibrary_);
if (face) {
FT_Done_Face(face);
}
if (library) {
FT_Done_FreeType(library);
}
}
}
bool GuiRenderer::IsReady() const {
@@ -504,6 +519,218 @@ void GuiRenderer::Resize(uint32_t width, uint32_t height, VkFormat format, VkRen
}
}
bool GuiRenderer::EnsureFreeTypeReady() {
if (freetypeReady_ || !fontConfig_.useFreeType) {
return freetypeReady_;
}
FT_Library library = nullptr;
if (FT_Init_FreeType(&library) != 0) {
if (logger_) {
logger_->Warn("GuiRenderer: FreeType initialization failed");
}
return false;
}
std::filesystem::path fontPath = ResolveFontPath();
if (fontPath.empty()) {
if (logger_) {
logger_->Warn("GuiRenderer: FreeType font path not set or not found");
}
FT_Done_FreeType(library);
return false;
}
FT_Face face = nullptr;
if (FT_New_Face(library, fontPath.string().c_str(), 0, &face) != 0) {
if (logger_) {
logger_->Warn("GuiRenderer: Failed to load FreeType font at " + fontPath.string());
}
FT_Done_FreeType(library);
return false;
}
ftLibrary_ = library;
ftFace_ = face;
freetypeReady_ = true;
return true;
}
std::filesystem::path GuiRenderer::ResolveFontPath() const {
if (!fontConfig_.fontPath.empty()) {
std::filesystem::path candidate = fontConfig_.fontPath;
if (candidate.is_absolute()) {
return candidate;
}
if (!scriptDirectory_.empty()) {
auto projectRoot = scriptDirectory_.parent_path();
if (!projectRoot.empty()) {
return std::filesystem::weakly_canonical(projectRoot / candidate);
}
return std::filesystem::weakly_canonical(scriptDirectory_ / candidate);
}
}
if (!scriptDirectory_.empty()) {
auto fallback = scriptDirectory_ / "assets" / "fonts" / "Roboto-Regular.ttf";
if (std::filesystem::exists(fallback)) {
return std::filesystem::weakly_canonical(fallback);
}
}
return {};
}
const GuiRenderer::GlyphBitmap* GuiRenderer::LoadGlyph(char c, int pixelSize) {
if (!EnsureFreeTypeReady()) {
return nullptr;
}
if (pixelSize <= 0) {
pixelSize = 1;
}
uint64_t key = (static_cast<uint64_t>(static_cast<uint32_t>(pixelSize)) << 32) |
static_cast<uint8_t>(c);
auto it = glyphCache_.find(key);
if (it != glyphCache_.end()) {
return &it->second;
}
FT_Face face = reinterpret_cast<FT_Face>(ftFace_);
if (!face) {
return nullptr;
}
if (currentFontSize_ != pixelSize) {
FT_Set_Pixel_Sizes(face, 0, static_cast<FT_UInt>(pixelSize));
currentFontSize_ = pixelSize;
}
if (FT_Load_Char(face, static_cast<FT_ULong>(static_cast<unsigned char>(c)), FT_LOAD_RENDER) != 0) {
return nullptr;
}
const FT_GlyphSlot slot = face->glyph;
GlyphBitmap glyph;
glyph.width = static_cast<int>(slot->bitmap.width);
glyph.height = static_cast<int>(slot->bitmap.rows);
glyph.pitch = static_cast<int>(slot->bitmap.pitch);
glyph.bearingX = slot->bitmap_left;
glyph.bearingY = slot->bitmap_top;
glyph.advance = static_cast<int>(slot->advance.x >> 6);
int pitch = glyph.pitch;
if (pitch < 0) {
pitch = -pitch;
}
glyph.pixels.assign(slot->bitmap.buffer, slot->bitmap.buffer + pitch * glyph.height);
auto inserted = glyphCache_.emplace(key, std::move(glyph));
return &inserted.first->second;
}
void GuiRenderer::RenderFreeTypeText(const GuiCommand& cmd,
const GuiCommand::RectData& activeClip,
const GuiCommand::RectData& bounds) {
FT_Face face = reinterpret_cast<FT_Face>(ftFace_);
if (!face) {
return;
}
int pixelSize = static_cast<int>(std::max(1.0f, cmd.fontSize));
if (currentFontSize_ != pixelSize) {
FT_Set_Pixel_Sizes(face, 0, static_cast<FT_UInt>(pixelSize));
currentFontSize_ = pixelSize;
}
float ascender = face->size ? static_cast<float>(face->size->metrics.ascender) / 64.0f : cmd.fontSize;
float lineHeight = face->size ? static_cast<float>(face->size->metrics.height) / 64.0f : cmd.fontSize;
float textWidth = 0.0f;
for (char c : cmd.text) {
const GlyphBitmap* glyph = LoadGlyph(c, pixelSize);
if (glyph) {
textWidth += static_cast<float>(glyph->advance);
}
}
float startX = bounds.x;
float startY = bounds.y;
if (cmd.alignX == "center") {
startX += (bounds.width - textWidth) * 0.5f;
} else if (cmd.alignX == "right") {
startX += bounds.width - textWidth;
}
if (cmd.alignY == "center") {
startY += (bounds.height - lineHeight) * 0.5f;
} else if (cmd.alignY == "bottom") {
startY += bounds.height - lineHeight;
}
float penX = startX;
float baseline = startY + ascender;
for (char c : cmd.text) {
const GlyphBitmap* glyph = LoadGlyph(c, pixelSize);
if (!glyph || glyph->pixels.empty()) {
penX += static_cast<float>(glyph ? glyph->advance : pixelSize / 2);
continue;
}
float glyphX = penX + static_cast<float>(glyph->bearingX);
float glyphY = baseline - static_cast<float>(glyph->bearingY);
int pitch = glyph->pitch < 0 ? -glyph->pitch : glyph->pitch;
for (int row = 0; row < glyph->height; ++row) {
for (int col = 0; col < glyph->width; ++col) {
uint8_t alpha = glyph->pixels[row * pitch + col];
if (alpha == 0) {
continue;
}
float a = cmd.color.a * (static_cast<float>(alpha) / 255.0f);
GuiColor color{cmd.color.r, cmd.color.g, cmd.color.b, a};
GuiCommand::RectData pixelRect{
glyphX + static_cast<float>(col),
glyphY + static_cast<float>(row),
1.0f,
1.0f
};
AddQuad(pixelRect, color, activeClip);
}
}
penX += static_cast<float>(glyph->advance);
}
}
void GuiRenderer::AddQuad(const GuiCommand::RectData& rect,
const GuiColor& color,
const GuiCommand::RectData& clipRect) {
GuiCommand::RectData clipped = IntersectRect(rect, clipRect);
if (!RectHasArea(clipped)) {
return;
}
auto toNDC = [this](float x, float y) -> std::pair<float, float> {
float width = viewportWidth_ == 0 ? 1.0f : static_cast<float>(viewportWidth_);
float height = viewportHeight_ == 0 ? 1.0f : static_cast<float>(viewportHeight_);
return {
(x / width) * 2.0f - 1.0f,
(y / height) * 2.0f - 1.0f
};
};
auto [x1, y1] = toNDC(clipped.x, clipped.y);
auto [x2, y2] = toNDC(clipped.x + clipped.width, clipped.y + clipped.height);
size_t baseIndex = vertices_.size();
vertices_.push_back({x1, y1, 0.0f, color.r, color.g, color.b, color.a});
vertices_.push_back({x2, y1, 0.0f, color.r, color.g, color.b, color.a});
vertices_.push_back({x2, y2, 0.0f, color.r, color.g, color.b, color.a});
vertices_.push_back({x1, y2, 0.0f, color.r, color.g, color.b, color.a});
indices_.push_back(static_cast<uint32_t>(baseIndex + 0));
indices_.push_back(static_cast<uint32_t>(baseIndex + 1));
indices_.push_back(static_cast<uint32_t>(baseIndex + 2));
indices_.push_back(static_cast<uint32_t>(baseIndex + 0));
indices_.push_back(static_cast<uint32_t>(baseIndex + 2));
indices_.push_back(static_cast<uint32_t>(baseIndex + 3));
}
void GuiRenderer::GenerateGuiGeometry(const std::vector<GuiCommand>& commands, uint32_t width, uint32_t height) {
vertices_.clear();
indices_.clear();
@@ -519,29 +746,6 @@ void GuiRenderer::GenerateGuiGeometry(const std::vector<GuiCommand>& commands, u
std::vector<GuiCommand::RectData> clipStack;
clipStack.push_back({0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)});
auto addQuad = [&](const GuiCommand::RectData& rect, const GuiColor& color,
const GuiCommand::RectData& clipRect) {
GuiCommand::RectData clipped = IntersectRect(rect, clipRect);
if (!RectHasArea(clipped)) {
return;
}
auto [x0, y0] = toNDC(clipped.x, clipped.y);
auto [x1, y1] = toNDC(clipped.x + clipped.width, clipped.y + clipped.height);
uint32_t baseIndex = static_cast<uint32_t>(vertices_.size());
vertices_.push_back({x0, y0, 0.0f, color.r, color.g, color.b, color.a});
vertices_.push_back({x1, y0, 0.0f, color.r, color.g, color.b, color.a});
vertices_.push_back({x1, y1, 0.0f, color.r, color.g, color.b, color.a});
vertices_.push_back({x0, y1, 0.0f, color.r, color.g, color.b, color.a});
indices_.push_back(baseIndex + 0);
indices_.push_back(baseIndex + 1);
indices_.push_back(baseIndex + 2);
indices_.push_back(baseIndex + 0);
indices_.push_back(baseIndex + 2);
indices_.push_back(baseIndex + 3);
};
auto addClippedPolygon = [&](const std::vector<ClipPoint>& polygon,
const GuiColor& color,
const GuiCommand::RectData& clipRect) {
@@ -580,7 +784,7 @@ void GuiRenderer::GenerateGuiGeometry(const std::vector<GuiCommand>& commands, u
}
if (cmd.type == GuiCommand::Type::Rect) {
addQuad(cmd.rect, cmd.color, activeClip);
AddQuad(cmd.rect, cmd.color, activeClip);
if (cmd.borderWidth > 0.0f && cmd.borderColor.a > 0.0f) {
float border = std::min(cmd.borderWidth, std::min(cmd.rect.width, cmd.rect.height));
if (border > 0.0f) {
@@ -591,14 +795,13 @@ void GuiRenderer::GenerateGuiGeometry(const std::vector<GuiCommand>& commands, u
GuiCommand::RectData left{cmd.rect.x, cmd.rect.y + border, border, innerHeight};
GuiCommand::RectData right{cmd.rect.x + cmd.rect.width - border,
cmd.rect.y + border, border, innerHeight};
addQuad(top, cmd.borderColor, activeClip);
addQuad(bottom, cmd.borderColor, activeClip);
addQuad(left, cmd.borderColor, activeClip);
addQuad(right, cmd.borderColor, activeClip);
AddQuad(top, cmd.borderColor, activeClip);
AddQuad(bottom, cmd.borderColor, activeClip);
AddQuad(left, cmd.borderColor, activeClip);
AddQuad(right, cmd.borderColor, activeClip);
}
}
} else if (cmd.type == GuiCommand::Type::Text) {
// Render text using 8x8 bitmap font
if (cmd.text.empty()) {
continue;
}
@@ -646,36 +849,31 @@ void GuiRenderer::GenerateGuiGeometry(const std::vector<GuiCommand>& commands, u
continue;
}
// Render each character as a small quad
float x = startX;
for (char c : cmd.text) {
// Only render printable ASCII characters
if (c >= 32 && c < 127) {
// Get character bitmap from font8x8_basic
const uint8_t* glyph = font8x8_basic[static_cast<unsigned char>(c)];
// Render each pixel of the 8x8 glyph
for (int row = 0; row < 8; ++row) {
uint8_t rowData = glyph[row];
for (int col = 0; col < 8; ++col) {
if (rowData & (1 << col)) {
// This pixel is on, render a small quad
// Add slight overlap (1.15x) for smoother appearance
const float pixelScale = 1.15f;
float pixelWidth = (charWidth / 8.0f) * pixelScale;
float pixelHeight = (charHeight / 8.0f) * pixelScale;
float px = x + col * (charWidth / 8.0f) - (pixelWidth - charWidth / 8.0f) * 0.5f;
float py = startY + row * (charHeight / 8.0f) - (pixelHeight - charHeight / 8.0f) * 0.5f;
float pw = pixelWidth;
float ph = pixelHeight;
GuiCommand::RectData pixelRect{px, py, pw, ph};
addQuad(pixelRect, cmd.color, textClip);
if (fontConfig_.useFreeType && EnsureFreeTypeReady()) {
RenderFreeTypeText(cmd, textClip, textBounds);
} else {
// Render text using 8x8 bitmap font
float x = startX;
for (char c : cmd.text) {
if (c >= 32 && c < 127) {
const uint8_t* glyph = font8x8_basic[static_cast<unsigned char>(c)];
for (int row = 0; row < 8; ++row) {
uint8_t rowData = glyph[row];
for (int col = 0; col < 8; ++col) {
if (rowData & (1 << col)) {
const float pixelScale = 1.15f;
float pixelWidth = (charWidth / 8.0f) * pixelScale;
float pixelHeight = (charHeight / 8.0f) * pixelScale;
float px = x + col * (charWidth / 8.0f) - (pixelWidth - charWidth / 8.0f) * 0.5f;
float py = startY + row * (charHeight / 8.0f) - (pixelHeight - charHeight / 8.0f) * 0.5f;
GuiCommand::RectData pixelRect{px, py, pixelWidth, pixelHeight};
AddQuad(pixelRect, cmd.color, textClip);
}
}
}
}
x += charWidth + charSpacing;
}
x += charWidth + charSpacing;
}
} else if (cmd.type == GuiCommand::Type::Svg) {
if (cmd.svgPath.empty()) {

View File

@@ -5,9 +5,11 @@
#include <memory>
#include <unordered_map>
#include <vector>
#include <cstdint>
#include <vulkan/vulkan.h>
#include "services/interfaces/config_types.hpp"
#include "services/interfaces/gui_types.hpp"
#include "services/interfaces/i_buffer_service.hpp"
#include "services/interfaces/i_logger.hpp"
@@ -36,6 +38,7 @@ class GuiRenderer {
public:
GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFormat swapchainFormat,
VkRenderPass renderPass, const std::filesystem::path& scriptDirectory,
const GuiFontConfig& fontConfig,
std::shared_ptr<IBufferService> bufferService, std::shared_ptr<ILogger> logger);
~GuiRenderer();
@@ -49,7 +52,26 @@ public:
bool IsReady() const;
private:
struct GlyphBitmap {
int width = 0;
int height = 0;
int pitch = 0;
int bearingX = 0;
int bearingY = 0;
int advance = 0;
std::vector<uint8_t> pixels;
};
const ParsedSvg* LoadSvg(const std::string& relativePath);
bool EnsureFreeTypeReady();
std::filesystem::path ResolveFontPath() const;
const GlyphBitmap* LoadGlyph(char c, int pixelSize);
void RenderFreeTypeText(const GuiCommand& cmd,
const GuiCommand::RectData& activeClip,
const GuiCommand::RectData& bounds);
void AddQuad(const GuiCommand::RectData& rect,
const GuiColor& color,
const GuiCommand::RectData& clipRect);
void CreatePipeline(VkRenderPass renderPass, VkExtent2D extent);
void CreateVertexAndIndexBuffers(size_t vertexCount, size_t indexCount);
@@ -68,6 +90,7 @@ private:
VkFormat swapchainFormat_;
VkRenderPass renderPass_;
std::filesystem::path scriptDirectory_;
GuiFontConfig fontConfig_;
// Pipeline resources
VkPipeline pipeline_ = VK_NULL_HANDLE;
@@ -89,6 +112,11 @@ private:
uint32_t viewportHeight_ = 0;
std::unordered_map<std::string, ParsedSvg> svgCache_;
std::unordered_map<std::string, std::vector<uint8_t>> shaderSpirvCache_;
std::unordered_map<uint64_t, GlyphBitmap> glyphCache_;
void* ftLibrary_ = nullptr;
void* ftFace_ = nullptr;
int currentFontSize_ = 0;
bool freetypeReady_ = false;
std::shared_ptr<IBufferService> bufferService_;
std::shared_ptr<ILogger> logger_;
};

View File

@@ -6,9 +6,11 @@
namespace sdl3cpp::services::impl {
GuiRendererService::GuiRendererService(std::shared_ptr<ILogger> logger,
std::shared_ptr<IBufferService> bufferService)
std::shared_ptr<IBufferService> bufferService,
std::shared_ptr<IConfigService> configService)
: logger_(std::move(logger)),
bufferService_(std::move(bufferService)) {
bufferService_(std::move(bufferService)),
configService_(std::move(configService)) {
if (logger_) {
logger_->Trace("GuiRendererService", "GuiRendererService",
"bufferService=" + std::string(bufferService_ ? "set" : "null"));
@@ -28,8 +30,13 @@ void GuiRendererService::Initialize(VkDevice device,
", renderPassIsNull=" + std::string(renderPass == VK_NULL_HANDLE ? "true" : "false") +
", resourcePath=" + resourcePath.string());
}
GuiFontConfig fontConfig;
if (configService_) {
fontConfig = configService_->GetGuiFontConfig();
}
renderer_ = std::make_unique<GuiRenderer>(
device, physicalDevice, format, renderPass, resourcePath, bufferService_, logger_);
device, physicalDevice, format, renderPass, resourcePath, fontConfig,
bufferService_, logger_);
}
void GuiRendererService::PrepareFrame(const std::vector<GuiCommand>& commands,

View File

@@ -1,6 +1,7 @@
#pragma once
#include "../interfaces/i_buffer_service.hpp"
#include "../interfaces/i_config_service.hpp"
#include "../interfaces/i_gui_renderer_service.hpp"
#include "../interfaces/i_logger.hpp"
#include "../../di/lifecycle.hpp"
@@ -12,7 +13,8 @@ namespace sdl3cpp::services::impl {
class GuiRendererService : public IGuiRendererService, public di::IShutdownable {
public:
GuiRendererService(std::shared_ptr<ILogger> logger,
std::shared_ptr<IBufferService> bufferService);
std::shared_ptr<IBufferService> bufferService,
std::shared_ptr<IConfigService> configService);
~GuiRendererService() override = default;
void Initialize(VkDevice device,
@@ -36,6 +38,7 @@ public:
private:
std::shared_ptr<ILogger> logger_;
std::shared_ptr<IBufferService> bufferService_;
std::shared_ptr<IConfigService> configService_;
std::unique_ptr<GuiRenderer> renderer_;
};

View File

@@ -7,6 +7,8 @@
#include <rapidjson/prettywriter.h>
#include <vulkan/vulkan.h>
#include <array>
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <optional>
@@ -19,6 +21,29 @@ static const std::vector<const char*> kDeviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME,
};
GraphicsBackendType ParseBackendType(const std::string& value) {
std::string lower = value;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lower == "vulkan") {
return GraphicsBackendType::Vulkan;
}
if (lower == "bgfx") {
return GraphicsBackendType::Bgfx;
}
throw std::runtime_error("graphics_backend.type must be 'vulkan' or 'bgfx'");
}
std::string BackendTypeToString(GraphicsBackendType type) {
switch (type) {
case GraphicsBackendType::Vulkan:
return "vulkan";
case GraphicsBackendType::Bgfx:
return "bgfx";
}
return "vulkan";
}
JsonConfigService::JsonConfigService(std::shared_ptr<ILogger> logger, const char* argv0)
: logger_(std::move(logger)), configJson_(), config_(RuntimeConfig{}) {
if (logger_) {
@@ -386,6 +411,129 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
}
}
if (document.HasMember("graphics_backend")) {
const auto& backendValue = document["graphics_backend"];
if (!backendValue.IsObject()) {
throw std::runtime_error("JSON member 'graphics_backend' must be an object");
}
if (backendValue.HasMember("type")) {
const auto& value = backendValue["type"];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'graphics_backend.type' must be a string");
}
config.graphicsBackend.backend = ParseBackendType(value.GetString());
}
if (backendValue.HasMember("bgfx_renderer")) {
const auto& value = backendValue["bgfx_renderer"];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'graphics_backend.bgfx_renderer' must be a string");
}
config.graphicsBackend.bgfxRenderer = value.GetString();
}
}
if (document.HasMember("materialx")) {
const auto& materialValue = document["materialx"];
if (!materialValue.IsObject()) {
throw std::runtime_error("JSON member 'materialx' must be an object");
}
if (materialValue.HasMember("enabled")) {
const auto& value = materialValue["enabled"];
if (!value.IsBool()) {
throw std::runtime_error("JSON member 'materialx.enabled' must be a boolean");
}
config.materialX.enabled = value.GetBool();
}
if (materialValue.HasMember("document")) {
const auto& value = materialValue["document"];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'materialx.document' must be a string");
}
config.materialX.documentPath = value.GetString();
}
if (materialValue.HasMember("shader_key")) {
const auto& value = materialValue["shader_key"];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'materialx.shader_key' must be a string");
}
config.materialX.shaderKey = value.GetString();
}
if (materialValue.HasMember("material")) {
const auto& value = materialValue["material"];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'materialx.material' must be a string");
}
config.materialX.materialName = value.GetString();
}
if (materialValue.HasMember("library_path")) {
const auto& value = materialValue["library_path"];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'materialx.library_path' must be a string");
}
config.materialX.libraryPath = value.GetString();
}
if (materialValue.HasMember("library_folders")) {
const auto& value = materialValue["library_folders"];
if (!value.IsArray()) {
throw std::runtime_error("JSON member 'materialx.library_folders' must be an array");
}
config.materialX.libraryFolders.clear();
for (rapidjson::SizeType i = 0; i < value.Size(); ++i) {
if (!value[i].IsString()) {
throw std::runtime_error("JSON member 'materialx.library_folders[" + std::to_string(i) + "]' must be a string");
}
config.materialX.libraryFolders.emplace_back(value[i].GetString());
}
}
if (materialValue.HasMember("use_constant_color")) {
const auto& value = materialValue["use_constant_color"];
if (!value.IsBool()) {
throw std::runtime_error("JSON member 'materialx.use_constant_color' must be a boolean");
}
config.materialX.useConstantColor = value.GetBool();
}
if (materialValue.HasMember("constant_color")) {
const auto& value = materialValue["constant_color"];
if (!value.IsArray() || value.Size() != 3) {
throw std::runtime_error("JSON member 'materialx.constant_color' must be an array of 3 numbers");
}
for (rapidjson::SizeType i = 0; i < 3; ++i) {
if (!value[i].IsNumber()) {
throw std::runtime_error("JSON member 'materialx.constant_color[" + std::to_string(i) + "]' must be a number");
}
config.materialX.constantColor[i] = static_cast<float>(value[i].GetDouble());
}
}
}
if (document.HasMember("gui_font")) {
const auto& fontValue = document["gui_font"];
if (!fontValue.IsObject()) {
throw std::runtime_error("JSON member 'gui_font' must be an object");
}
if (fontValue.HasMember("use_freetype")) {
const auto& value = fontValue["use_freetype"];
if (!value.IsBool()) {
throw std::runtime_error("JSON member 'gui_font.use_freetype' must be a boolean");
}
config.guiFont.useFreeType = value.GetBool();
}
if (fontValue.HasMember("font_path")) {
const auto& value = fontValue["font_path"];
if (!value.IsString()) {
throw std::runtime_error("JSON member 'gui_font.font_path' must be a string");
}
config.guiFont.fontPath = value.GetString();
}
if (fontValue.HasMember("font_size")) {
const auto& value = fontValue["font_size"];
if (!value.IsNumber()) {
throw std::runtime_error("JSON member 'gui_font.font_size' must be a number");
}
config.guiFont.fontSize = static_cast<float>(value.GetDouble());
}
}
if (document.HasMember("gui_opacity")) {
const auto& value = document["gui_opacity"];
if (!value.IsNumber()) {
@@ -442,6 +590,50 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config,
allocator);
document.AddMember("render_graph", renderGraphObject, allocator);
rapidjson::Value backendObject(rapidjson::kObjectType);
backendObject.AddMember("type",
rapidjson::Value(BackendTypeToString(config.graphicsBackend.backend).c_str(), allocator),
allocator);
backendObject.AddMember("bgfx_renderer",
rapidjson::Value(config.graphicsBackend.bgfxRenderer.c_str(), allocator),
allocator);
document.AddMember("graphics_backend", backendObject, allocator);
rapidjson::Value materialObject(rapidjson::kObjectType);
materialObject.AddMember("enabled", config.materialX.enabled, allocator);
materialObject.AddMember("document",
rapidjson::Value(config.materialX.documentPath.string().c_str(), allocator),
allocator);
materialObject.AddMember("shader_key",
rapidjson::Value(config.materialX.shaderKey.c_str(), allocator),
allocator);
materialObject.AddMember("material",
rapidjson::Value(config.materialX.materialName.c_str(), allocator),
allocator);
materialObject.AddMember("library_path",
rapidjson::Value(config.materialX.libraryPath.string().c_str(), allocator),
allocator);
rapidjson::Value libraryFolders(rapidjson::kArrayType);
for (const auto& folder : config.materialX.libraryFolders) {
libraryFolders.PushBack(rapidjson::Value(folder.c_str(), allocator), allocator);
}
materialObject.AddMember("library_folders", libraryFolders, allocator);
materialObject.AddMember("use_constant_color", config.materialX.useConstantColor, allocator);
rapidjson::Value constantColor(rapidjson::kArrayType);
constantColor.PushBack(config.materialX.constantColor[0], allocator);
constantColor.PushBack(config.materialX.constantColor[1], allocator);
constantColor.PushBack(config.materialX.constantColor[2], allocator);
materialObject.AddMember("constant_color", constantColor, allocator);
document.AddMember("materialx", materialObject, allocator);
rapidjson::Value fontObject(rapidjson::kObjectType);
fontObject.AddMember("use_freetype", config.guiFont.useFreeType, allocator);
fontObject.AddMember("font_path",
rapidjson::Value(config.guiFont.fontPath.string().c_str(), allocator),
allocator);
fontObject.AddMember("font_size", config.guiFont.fontSize, allocator);
document.AddMember("gui_font", fontObject, allocator);
rapidjson::Value bindingsObject(rapidjson::kObjectType);
auto addBindingMember = [&](const char* name, const std::string& value) {
rapidjson::Value nameValue(name, allocator);

View File

@@ -94,6 +94,24 @@ public:
}
return config_.renderGraph;
}
const GraphicsBackendConfig& GetGraphicsBackendConfig() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetGraphicsBackendConfig");
}
return config_.graphicsBackend;
}
const MaterialXConfig& GetMaterialXConfig() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetMaterialXConfig");
}
return config_.materialX;
}
const GuiFontConfig& GetGuiFontConfig() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetGuiFontConfig");
}
return config_.guiFont;
}
const std::string& GetConfigJson() const override {
if (logger_) {
logger_->Trace("JsonConfigService", "GetConfigJson");

View File

@@ -0,0 +1,113 @@
#include "materialx_shader_generator.hpp"
#include <MaterialXCore/Document.h>
#include <MaterialXFormat/File.h>
#include <MaterialXGenGlsl/VkShaderGenerator.h>
#include <MaterialXGenShader/GenContext.h>
#include <MaterialXGenShader/Util.h>
#include <MaterialXRender/Util.h>
#include <stdexcept>
namespace sdl3cpp::services::impl {
namespace mx = MaterialX;
MaterialXShaderGenerator::MaterialXShaderGenerator(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
std::filesystem::path MaterialXShaderGenerator::ResolvePath(
const std::filesystem::path& path,
const std::filesystem::path& scriptDirectory) const {
if (path.empty()) {
return {};
}
if (path.is_absolute()) {
return path;
}
std::filesystem::path base = scriptDirectory;
if (!base.empty()) {
auto projectRoot = base.parent_path();
if (!projectRoot.empty()) {
return std::filesystem::weakly_canonical(projectRoot / path);
}
}
return std::filesystem::weakly_canonical(path);
}
ShaderPaths MaterialXShaderGenerator::Generate(const MaterialXConfig& config,
const std::filesystem::path& scriptDirectory) const {
if (!config.enabled) {
return {};
}
mx::FileSearchPath searchPath;
std::filesystem::path libraryPath = ResolvePath(config.libraryPath, scriptDirectory);
if (libraryPath.empty() && !scriptDirectory.empty()) {
auto fallback = scriptDirectory.parent_path() / "MaterialX" / "libraries";
if (std::filesystem::exists(fallback)) {
libraryPath = fallback;
}
}
if (!libraryPath.empty()) {
searchPath.append(mx::FilePath(libraryPath.string()));
}
mx::DocumentPtr stdLib = mx::createDocument();
if (!config.libraryFolders.empty()) {
mx::FilePathVec folders;
for (const auto& folder : config.libraryFolders) {
folders.emplace_back(folder);
}
mx::loadLibraries(folders, searchPath, stdLib);
}
mx::ShaderGeneratorPtr generator = mx::VkShaderGenerator::create();
mx::GenContext context(generator);
context.registerSourceCodeSearchPath(searchPath);
mx::ShaderPtr shader;
if (config.useConstantColor) {
mx::Color3 color(config.constantColor[0], config.constantColor[1], config.constantColor[2]);
shader = mx::createConstantShader(context, stdLib, config.shaderKey, color);
} else {
if (config.documentPath.empty()) {
throw std::runtime_error("MaterialX document path is required when use_constant_color is false");
}
std::filesystem::path documentPath = ResolvePath(config.documentPath, scriptDirectory);
if (documentPath.empty()) {
throw std::runtime_error("MaterialX document path could not be resolved");
}
mx::DocumentPtr document = mx::createDocument();
mx::readFromXmlFile(document, mx::FilePath(documentPath.string()), &searchPath);
document->importLibrary(stdLib);
mx::TypedElementPtr element;
if (!config.materialName.empty()) {
element = document->getMaterial(config.materialName);
}
if (!element) {
auto renderables = mx::findRenderableElements(document);
if (!renderables.empty()) {
element = renderables.front();
}
}
if (!element) {
throw std::runtime_error("MaterialX document has no renderable elements");
}
shader = mx::createShader(config.shaderKey, context, element);
}
if (!shader) {
throw std::runtime_error("MaterialX shader generation failed");
}
ShaderPaths paths;
paths.vertexSource = shader->getSourceCode(mx::Stage::VERTEX);
paths.fragmentSource = shader->getSourceCode(mx::Stage::PIXEL);
return paths;
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,24 @@
#pragma once
#include "../interfaces/config_types.hpp"
#include "../interfaces/graphics_types.hpp"
#include "../interfaces/i_logger.hpp"
#include <filesystem>
#include <memory>
namespace sdl3cpp::services::impl {
class MaterialXShaderGenerator {
public:
MaterialXShaderGenerator(std::shared_ptr<ILogger> logger);
ShaderPaths Generate(const MaterialXConfig& config,
const std::filesystem::path& scriptDirectory) const;
private:
std::filesystem::path ResolvePath(const std::filesystem::path& path,
const std::filesystem::path& scriptDirectory) const;
std::shared_ptr<ILogger> logger_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,53 @@
#include "null_gui_service.hpp"
#include <string>
namespace sdl3cpp::services::impl {
NullGuiService::NullGuiService(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {
if (logger_) {
logger_->Trace("NullGuiService", "NullGuiService");
}
}
void NullGuiService::Initialize(VkDevice device,
VkPhysicalDevice physicalDevice,
VkFormat format,
VkRenderPass renderPass,
const std::filesystem::path& resourcePath) {
if (logger_) {
logger_->Trace("NullGuiService", "Initialize");
}
}
void NullGuiService::PrepareFrame(const std::vector<GuiCommand>& commands,
uint32_t width,
uint32_t height) {
if (logger_) {
logger_->Trace("NullGuiService", "PrepareFrame",
"commands.size=" + std::to_string(commands.size()));
}
}
void NullGuiService::RenderToSwapchain(VkCommandBuffer commandBuffer, VkImage image) {
if (logger_) {
logger_->Trace("NullGuiService", "RenderToSwapchain");
}
}
void NullGuiService::Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass) {
if (logger_) {
logger_->Trace("NullGuiService", "Resize",
"width=" + std::to_string(width) +
", height=" + std::to_string(height));
}
}
void NullGuiService::Shutdown() {
if (logger_) {
logger_->Trace("NullGuiService", "Shutdown");
}
}
} // namespace sdl3cpp::services::impl

View File

@@ -0,0 +1,29 @@
#pragma once
#include "../interfaces/i_gui_service.hpp"
#include "../interfaces/i_logger.hpp"
#include <memory>
namespace sdl3cpp::services::impl {
class NullGuiService : public IGuiService {
public:
explicit NullGuiService(std::shared_ptr<ILogger> logger);
void Initialize(VkDevice device,
VkPhysicalDevice physicalDevice,
VkFormat format,
VkRenderPass renderPass,
const std::filesystem::path& resourcePath) override;
void PrepareFrame(const std::vector<GuiCommand>& commands,
uint32_t width,
uint32_t height) override;
void RenderToSwapchain(VkCommandBuffer commandBuffer, VkImage image) override;
void Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass) override;
void Shutdown() override;
private:
std::shared_ptr<ILogger> logger_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -15,12 +15,16 @@
namespace sdl3cpp::services::impl {
ShaderScriptService::ShaderScriptService(std::shared_ptr<IScriptEngineService> engineService,
std::shared_ptr<IConfigService> configService,
std::shared_ptr<ILogger> logger)
: engineService_(std::move(engineService)),
logger_(std::move(logger)) {
configService_(std::move(configService)),
logger_(std::move(logger)),
materialxGenerator_(logger_) {
if (logger_) {
logger_->Trace("ShaderScriptService", "ShaderScriptService",
"engineService=" + std::string(engineService_ ? "set" : "null"));
"engineService=" + std::string(engineService_ ? "set" : "null") +
", configService=" + std::string(configService_ ? "set" : "null"));
}
}
@@ -65,12 +69,32 @@ std::unordered_map<std::string, ShaderPaths> ShaderScriptService::LoadShaderPath
}
lua_pop(L, 1);
if (configService_) {
const auto& materialConfig = configService_->GetMaterialXConfig();
if (materialConfig.enabled) {
try {
ShaderPaths materialShader = materialxGenerator_.Generate(
materialConfig,
engineService_ ? engineService_->GetScriptDirectory() : std::filesystem::path{});
if (!materialConfig.shaderKey.empty()) {
shaderMap[materialConfig.shaderKey] = std::move(materialShader);
}
} catch (const std::exception& ex) {
if (logger_) {
logger_->Error("MaterialX shader generation failed: " + std::string(ex.what()));
}
}
}
}
if (shaderMap.empty()) {
if (logger_) {
logger_->Error("'get_shader_paths' did not return any shader variants");
}
throw std::runtime_error("'get_shader_paths' did not return any shader variants");
}
return shaderMap;
}

View File

@@ -2,7 +2,9 @@
#include "../interfaces/i_shader_script_service.hpp"
#include "../interfaces/i_script_engine_service.hpp"
#include "../interfaces/i_config_service.hpp"
#include "../interfaces/i_logger.hpp"
#include "materialx_shader_generator.hpp"
#include <memory>
struct lua_State;
@@ -15,6 +17,7 @@ namespace sdl3cpp::services::impl {
class ShaderScriptService : public IShaderScriptService {
public:
ShaderScriptService(std::shared_ptr<IScriptEngineService> engineService,
std::shared_ptr<IConfigService> configService,
std::shared_ptr<ILogger> logger);
std::unordered_map<std::string, ShaderPaths> LoadShaderPathsMap() override;
@@ -25,7 +28,9 @@ private:
std::string ResolveShaderPath(const std::string& path) const;
std::shared_ptr<IScriptEngineService> engineService_;
std::shared_ptr<IConfigService> configService_;
std::shared_ptr<ILogger> logger_;
MaterialXShaderGenerator materialxGenerator_;
};
} // namespace sdl3cpp::services::impl

View File

@@ -5,6 +5,7 @@
#include <filesystem>
#include <string>
#include <unordered_map>
#include <vector>
namespace sdl3cpp::services {
@@ -89,6 +90,41 @@ struct RenderGraphConfig {
std::string functionName = "get_render_graph";
};
enum class GraphicsBackendType {
Vulkan,
Bgfx
};
struct GraphicsBackendConfig {
GraphicsBackendType backend = GraphicsBackendType::Vulkan;
std::string bgfxRenderer = "vulkan";
};
struct MaterialXConfig {
bool enabled = false;
std::filesystem::path documentPath;
std::string shaderKey = "materialx";
std::string materialName;
std::filesystem::path libraryPath;
std::vector<std::string> libraryFolders = {
"stdlib",
"pbrlib",
"lights",
"bxdf",
"cmlib",
"nprlib",
"targets"
};
bool useConstantColor = false;
std::array<float, 3> constantColor = {1.0f, 1.0f, 1.0f};
};
struct GuiFontConfig {
bool useFreeType = false;
std::filesystem::path fontPath;
float fontSize = 18.0f;
};
/**
* @brief Runtime configuration values used across services.
*/
@@ -102,6 +138,9 @@ struct RuntimeConfig {
InputBindings inputBindings{};
AtmosphericsConfig atmospherics{};
RenderGraphConfig renderGraph{};
GraphicsBackendConfig graphicsBackend{};
MaterialXConfig materialX{};
GuiFontConfig guiFont{};
float guiOpacity = 1.0f;
};

View File

@@ -72,6 +72,24 @@ public:
*/
virtual const RenderGraphConfig& GetRenderGraphConfig() const = 0;
/**
* @brief Get graphics backend settings.
* @return Graphics backend configuration
*/
virtual const GraphicsBackendConfig& GetGraphicsBackendConfig() const = 0;
/**
* @brief Get MaterialX settings.
* @return MaterialX configuration
*/
virtual const MaterialXConfig& GetMaterialXConfig() const = 0;
/**
* @brief Get GUI font settings.
* @return GUI font configuration
*/
virtual const GuiFontConfig& GetGuiFontConfig() const = 0;
/**
* @brief Get the full JSON configuration as a string.
*

View File

@@ -2,6 +2,7 @@
#include "services/impl/script_engine_service.hpp"
#include "services/impl/scene_script_service.hpp"
#include "services/impl/shader_script_service.hpp"
#include "services/interfaces/i_config_service.hpp"
#include <array>
#include <cmath>
@@ -68,7 +69,8 @@ int main() {
engineService->Initialize();
sdl3cpp::services::impl::SceneScriptService sceneService(engineService, logger);
sdl3cpp::services::impl::ShaderScriptService shaderService(engineService, logger);
std::shared_ptr<sdl3cpp::services::IConfigService> configService;
sdl3cpp::services::impl::ShaderScriptService shaderService(engineService, configService, logger);
auto objects = sceneService.LoadSceneObjects();
Assert(objects.size() == 1, "expected exactly one scene object", failures);