diff --git a/CMakeLists.txt b/CMakeLists.txt index c039820..0a96508 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 $<$>:src/services/impl/vulkan_graphics_backend.cpp> + $<$>:src/services/impl/bgfx_graphics_backend.cpp> $<$: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 diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index 2099347..29a5b04 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -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(); - auto vulkanDeviceService = registry_.GetService(); - auto swapchainService = registry_.GetService(); - 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() && + registry_.HasService()) { + auto vulkanDeviceService = registry_.GetService(); + auto swapchainService = registry_.GetService(); + 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( registry_.GetService(), runtimeConfig_); + auto configService = registry_.GetService(); + bool useBgfx = false; + if (configService) { + useBgfx = configService->GetGraphicsBackendConfig().backend == services::GraphicsBackendType::Bgfx; + } // ECS service (entt registry) registry_.RegisterService( @@ -281,6 +296,7 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService()); registry_.RegisterService( registry_.GetService(), + registry_.GetService(), registry_.GetService()); registry_.RegisterService( registry_.GetService(), @@ -299,53 +315,63 @@ void ServiceBasedApp::RegisterServices() { inputService->SetGuiScriptService(guiScriptService.get()); } - // Vulkan device service - registry_.RegisterService( - registry_.GetService()); + std::shared_ptr graphicsBackend; + if (!useBgfx) { + // Vulkan device service + registry_.RegisterService( + registry_.GetService()); - // Swapchain service - registry_.RegisterService( - registry_.GetService(), - registry_.GetService(), - registry_.GetService()); + // Swapchain service + registry_.RegisterService( + registry_.GetService(), + registry_.GetService(), + registry_.GetService()); - // Pipeline service - registry_.RegisterService( - registry_.GetService(), - registry_.GetService()); + // Pipeline service + registry_.RegisterService( + registry_.GetService(), + registry_.GetService()); - // Buffer service - registry_.RegisterService( - registry_.GetService(), - registry_.GetService()); + // Buffer service + registry_.RegisterService( + registry_.GetService(), + registry_.GetService()); - // GUI renderer service (needed by render command service and GUI service) - registry_.RegisterService( - registry_.GetService(), - registry_.GetService()); - 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( + registry_.GetService(), + registry_.GetService(), + registry_.GetService()); + logger_->Trace("ServiceBasedApp", "RegisterServices", + "Registered GUI renderer service before render command service"); - // Render command service - registry_.RegisterService( - registry_.GetService(), - registry_.GetService(), - registry_.GetService(), - registry_.GetService(), - registry_.GetService(), - std::static_pointer_cast(registry_.GetService()), - registry_.GetService()); + // Render command service + registry_.RegisterService( + registry_.GetService(), + registry_.GetService(), + registry_.GetService(), + registry_.GetService(), + registry_.GetService(), + std::static_pointer_cast(registry_.GetService()), + registry_.GetService()); - // Graphics service (facade) - registry_.RegisterService( - registry_.GetService(), - std::make_shared( + graphicsBackend = std::make_shared( registry_.GetService(), registry_.GetService(), registry_.GetService(), registry_.GetService(), registry_.GetService(), - registry_.GetService()), + registry_.GetService()); + } else { + graphicsBackend = std::make_shared( + registry_.GetService(), + registry_.GetService()); + } + + // Graphics service (facade) + registry_.RegisterService( + registry_.GetService(), + graphicsBackend, registry_.GetService()); // Scene service @@ -355,9 +381,14 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService()); // GUI service - registry_.RegisterService( - registry_.GetService(), - registry_.GetService()); + if (!useBgfx) { + registry_.RegisterService( + registry_.GetService(), + registry_.GetService()); + } else { + registry_.RegisterService( + registry_.GetService()); + } // Physics service registry_.RegisterService( diff --git a/src/services/impl/bgfx_graphics_backend.cpp b/src/services/impl/bgfx_graphics_backend.cpp new file mode 100644 index 0000000..1e56a65 --- /dev/null +++ b/src/services/impl/bgfx_graphics_backend.cpp @@ -0,0 +1,461 @@ +#include "bgfx_graphics_backend.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(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 configService, + std::shared_ptr 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(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(static_cast(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(window); + int width = 0; + int height = 0; + if (sdlWindow) { + SDL_GetWindowSizeInPixels(sdlWindow, &width, &height); + } + viewportWidth_ = static_cast(std::max(1, width)); + viewportHeight_ = static_cast(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(1); +} + +void BgfxGraphicsBackend::DestroyDevice(GraphicsDeviceHandle device) { + if (logger_) { + logger_->Trace("BgfxGraphicsBackend", "DestroyDevice"); + } +} + +std::vector BgfxGraphicsBackend::ReadShaderSource(const std::string& path, + const std::string& source) const { + if (!source.empty()) { + return std::vector(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((std::istreambuf_iterator(sourceFile)), + std::istreambuf_iterator()); +} + +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 spirv(result.cbegin(), result.cend()); + const bgfx::Memory* mem = bgfx::copy(spirv.data(), + static_cast(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 vertexBytes = ReadShaderSource(shaderPaths.vertex, shaderPaths.vertexSource); + std::vector 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(); + entry->program = program; + GraphicsPipelineHandle handle = reinterpret_cast(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& 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(data.size() / sizeof(core::Vertex)); + const bgfx::Memory* mem = bgfx::copy(data.data(), static_cast(data.size())); + bgfx::VertexBufferHandle buffer = bgfx::createVertexBuffer(mem, vertexLayout_); + + auto entry = std::make_unique(); + entry->handle = buffer; + entry->vertexCount = vertexCount; + GraphicsBufferHandle handle = reinterpret_cast(entry.get()); + vertexBuffers_.emplace(handle, std::move(entry)); + return handle; +} + +GraphicsBufferHandle BgfxGraphicsBackend::CreateIndexBuffer(GraphicsDeviceHandle device, + const std::vector& 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(data.size() / sizeof(uint16_t)); + const bgfx::Memory* mem = bgfx::copy(data.data(), static_cast(data.size())); + bgfx::IndexBufferHandle buffer = bgfx::createIndexBuffer(mem); + + auto entry = std::make_unique(); + entry->handle = buffer; + entry->indexCount = indexCount; + GraphicsBufferHandle handle = reinterpret_cast(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& 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& 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(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 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 diff --git a/src/services/impl/bgfx_graphics_backend.hpp b/src/services/impl/bgfx_graphics_backend.hpp new file mode 100644 index 0000000..4e848bc --- /dev/null +++ b/src/services/impl/bgfx_graphics_backend.hpp @@ -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 +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +class BgfxGraphicsBackend : public IGraphicsBackend { +public: + BgfxGraphicsBackend(std::shared_ptr configService, + std::shared_ptr 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& data) override; + GraphicsBufferHandle CreateIndexBuffer(GraphicsDeviceHandle device, + const std::vector& data) override; + void DestroyBuffer(GraphicsDeviceHandle device, GraphicsBufferHandle buffer) override; + bool BeginFrame(GraphicsDeviceHandle device) override; + bool EndFrame(GraphicsDeviceHandle device) override; + void SetViewProjection(const std::array& 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& modelMatrix) override; + GraphicsDeviceHandle GetPhysicalDevice() const override; + std::pair 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 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 configService_; + std::shared_ptr logger_; + bgfx::VertexLayout vertexLayout_; + std::unordered_map> pipelines_; + std::unordered_map> vertexBuffers_; + std::unordered_map> indexBuffers_; + std::array viewProj_{}; + uint32_t viewportWidth_ = 0; + uint32_t viewportHeight_ = 0; + bool initialized_ = false; + bgfx::ViewId viewId_ = 0; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/gui_renderer.cpp b/src/services/impl/gui_renderer.cpp index cb9ef6d..e4236a6 100644 --- a/src/services/impl/gui_renderer.cpp +++ b/src/services/impl/gui_renderer.cpp @@ -1,5 +1,8 @@ #include "gui_renderer.hpp" +#include +#include FT_FREETYPE_H + #include #include #include @@ -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 bufferService, std::shared_ptr 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(ftFace_); + FT_Library library = reinterpret_cast(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(static_cast(pixelSize)) << 32) | + static_cast(c); + auto it = glyphCache_.find(key); + if (it != glyphCache_.end()) { + return &it->second; + } + + FT_Face face = reinterpret_cast(ftFace_); + if (!face) { + return nullptr; + } + if (currentFontSize_ != pixelSize) { + FT_Set_Pixel_Sizes(face, 0, static_cast(pixelSize)); + currentFontSize_ = pixelSize; + } + + if (FT_Load_Char(face, static_cast(static_cast(c)), FT_LOAD_RENDER) != 0) { + return nullptr; + } + + const FT_GlyphSlot slot = face->glyph; + GlyphBitmap glyph; + glyph.width = static_cast(slot->bitmap.width); + glyph.height = static_cast(slot->bitmap.rows); + glyph.pitch = static_cast(slot->bitmap.pitch); + glyph.bearingX = slot->bitmap_left; + glyph.bearingY = slot->bitmap_top; + glyph.advance = static_cast(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(ftFace_); + if (!face) { + return; + } + int pixelSize = static_cast(std::max(1.0f, cmd.fontSize)); + if (currentFontSize_ != pixelSize) { + FT_Set_Pixel_Sizes(face, 0, static_cast(pixelSize)); + currentFontSize_ = pixelSize; + } + + float ascender = face->size ? static_cast(face->size->metrics.ascender) / 64.0f : cmd.fontSize; + float lineHeight = face->size ? static_cast(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(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(glyph ? glyph->advance : pixelSize / 2); + continue; + } + + float glyphX = penX + static_cast(glyph->bearingX); + float glyphY = baseline - static_cast(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(alpha) / 255.0f); + GuiColor color{cmd.color.r, cmd.color.g, cmd.color.b, a}; + GuiCommand::RectData pixelRect{ + glyphX + static_cast(col), + glyphY + static_cast(row), + 1.0f, + 1.0f + }; + AddQuad(pixelRect, color, activeClip); + } + } + penX += static_cast(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 width = viewportWidth_ == 0 ? 1.0f : static_cast(viewportWidth_); + float height = viewportHeight_ == 0 ? 1.0f : static_cast(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(baseIndex + 0)); + indices_.push_back(static_cast(baseIndex + 1)); + indices_.push_back(static_cast(baseIndex + 2)); + indices_.push_back(static_cast(baseIndex + 0)); + indices_.push_back(static_cast(baseIndex + 2)); + indices_.push_back(static_cast(baseIndex + 3)); +} + void GuiRenderer::GenerateGuiGeometry(const std::vector& commands, uint32_t width, uint32_t height) { vertices_.clear(); indices_.clear(); @@ -519,29 +746,6 @@ void GuiRenderer::GenerateGuiGeometry(const std::vector& commands, u std::vector clipStack; clipStack.push_back({0.0f, 0.0f, static_cast(width), static_cast(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(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& polygon, const GuiColor& color, const GuiCommand::RectData& clipRect) { @@ -580,7 +784,7 @@ void GuiRenderer::GenerateGuiGeometry(const std::vector& 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& 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& 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(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(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()) { diff --git a/src/services/impl/gui_renderer.hpp b/src/services/impl/gui_renderer.hpp index 3323ff6..d91a5f0 100644 --- a/src/services/impl/gui_renderer.hpp +++ b/src/services/impl/gui_renderer.hpp @@ -5,9 +5,11 @@ #include #include #include +#include #include +#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 bufferService, std::shared_ptr 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 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 svgCache_; std::unordered_map> shaderSpirvCache_; + std::unordered_map glyphCache_; + void* ftLibrary_ = nullptr; + void* ftFace_ = nullptr; + int currentFontSize_ = 0; + bool freetypeReady_ = false; std::shared_ptr bufferService_; std::shared_ptr logger_; }; diff --git a/src/services/impl/gui_renderer_service.cpp b/src/services/impl/gui_renderer_service.cpp index e9b5826..f89945c 100644 --- a/src/services/impl/gui_renderer_service.cpp +++ b/src/services/impl/gui_renderer_service.cpp @@ -6,9 +6,11 @@ namespace sdl3cpp::services::impl { GuiRendererService::GuiRendererService(std::shared_ptr logger, - std::shared_ptr bufferService) + std::shared_ptr bufferService, + std::shared_ptr 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( - device, physicalDevice, format, renderPass, resourcePath, bufferService_, logger_); + device, physicalDevice, format, renderPass, resourcePath, fontConfig, + bufferService_, logger_); } void GuiRendererService::PrepareFrame(const std::vector& commands, diff --git a/src/services/impl/gui_renderer_service.hpp b/src/services/impl/gui_renderer_service.hpp index b9e9108..302d19c 100644 --- a/src/services/impl/gui_renderer_service.hpp +++ b/src/services/impl/gui_renderer_service.hpp @@ -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 logger, - std::shared_ptr bufferService); + std::shared_ptr bufferService, + std::shared_ptr configService); ~GuiRendererService() override = default; void Initialize(VkDevice device, @@ -36,6 +38,7 @@ public: private: std::shared_ptr logger_; std::shared_ptr bufferService_; + std::shared_ptr configService_; std::unique_ptr renderer_; }; diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index 5e8e93b..738bbe2 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,6 +21,29 @@ static const std::vector 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(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 logger, const char* argv0) : logger_(std::move(logger)), configJson_(), config_(RuntimeConfig{}) { if (logger_) { @@ -386,6 +411,129 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr 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(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(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); diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp index acf8a32..9e4841e 100644 --- a/src/services/impl/json_config_service.hpp +++ b/src/services/impl/json_config_service.hpp @@ -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"); diff --git a/src/services/impl/materialx_shader_generator.cpp b/src/services/impl/materialx_shader_generator.cpp new file mode 100644 index 0000000..10f90c8 --- /dev/null +++ b/src/services/impl/materialx_shader_generator.cpp @@ -0,0 +1,113 @@ +#include "materialx_shader_generator.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace sdl3cpp::services::impl { +namespace mx = MaterialX; + +MaterialXShaderGenerator::MaterialXShaderGenerator(std::shared_ptr 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 diff --git a/src/services/impl/materialx_shader_generator.hpp b/src/services/impl/materialx_shader_generator.hpp new file mode 100644 index 0000000..85e4f20 --- /dev/null +++ b/src/services/impl/materialx_shader_generator.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "../interfaces/config_types.hpp" +#include "../interfaces/graphics_types.hpp" +#include "../interfaces/i_logger.hpp" +#include +#include + +namespace sdl3cpp::services::impl { + +class MaterialXShaderGenerator { +public: + MaterialXShaderGenerator(std::shared_ptr 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 logger_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/null_gui_service.cpp b/src/services/impl/null_gui_service.cpp new file mode 100644 index 0000000..ea0d48b --- /dev/null +++ b/src/services/impl/null_gui_service.cpp @@ -0,0 +1,53 @@ +#include "null_gui_service.hpp" + +#include + +namespace sdl3cpp::services::impl { + +NullGuiService::NullGuiService(std::shared_ptr 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& 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 diff --git a/src/services/impl/null_gui_service.hpp b/src/services/impl/null_gui_service.hpp new file mode 100644 index 0000000..b4affa1 --- /dev/null +++ b/src/services/impl/null_gui_service.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "../interfaces/i_gui_service.hpp" +#include "../interfaces/i_logger.hpp" +#include + +namespace sdl3cpp::services::impl { + +class NullGuiService : public IGuiService { +public: + explicit NullGuiService(std::shared_ptr logger); + + void Initialize(VkDevice device, + VkPhysicalDevice physicalDevice, + VkFormat format, + VkRenderPass renderPass, + const std::filesystem::path& resourcePath) override; + void PrepareFrame(const std::vector& 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 logger_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/shader_script_service.cpp b/src/services/impl/shader_script_service.cpp index 96ec548..1c44445 100644 --- a/src/services/impl/shader_script_service.cpp +++ b/src/services/impl/shader_script_service.cpp @@ -15,12 +15,16 @@ namespace sdl3cpp::services::impl { ShaderScriptService::ShaderScriptService(std::shared_ptr engineService, + std::shared_ptr configService, std::shared_ptr 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 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; } diff --git a/src/services/impl/shader_script_service.hpp b/src/services/impl/shader_script_service.hpp index 07c255c..50bedc1 100644 --- a/src/services/impl/shader_script_service.hpp +++ b/src/services/impl/shader_script_service.hpp @@ -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 struct lua_State; @@ -15,6 +17,7 @@ namespace sdl3cpp::services::impl { class ShaderScriptService : public IShaderScriptService { public: ShaderScriptService(std::shared_ptr engineService, + std::shared_ptr configService, std::shared_ptr logger); std::unordered_map LoadShaderPathsMap() override; @@ -25,7 +28,9 @@ private: std::string ResolveShaderPath(const std::string& path) const; std::shared_ptr engineService_; + std::shared_ptr configService_; std::shared_ptr logger_; + MaterialXShaderGenerator materialxGenerator_; }; } // namespace sdl3cpp::services::impl diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp index b9f56bd..c6b807b 100644 --- a/src/services/interfaces/config_types.hpp +++ b/src/services/interfaces/config_types.hpp @@ -5,6 +5,7 @@ #include #include #include +#include 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 libraryFolders = { + "stdlib", + "pbrlib", + "lights", + "bxdf", + "cmlib", + "nprlib", + "targets" + }; + bool useConstantColor = false; + std::array 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; }; diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index d3c1198..1001bfe 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -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. * diff --git a/tests/test_cube_script.cpp b/tests/test_cube_script.cpp index 5a35c3f..a867541 100644 --- a/tests/test_cube_script.cpp +++ b/tests/test_cube_script.cpp @@ -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 #include @@ -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 configService; + sdl3cpp::services::impl::ShaderScriptService shaderService(engineService, configService, logger); auto objects = sceneService.LoadSceneObjects(); Assert(objects.size() == 1, "expected exactly one scene object", failures);