#include "bgfx_graphics_backend.hpp" #include "bgfx_shader_compiler.hpp" #include "../interfaces/i_pipeline_compiler_service.hpp" #include #include #include #include #include #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; } glm::mat4 ToMat4(const std::array& value) { return glm::make_mat4(value.data()); } bool IsIdentityMatrix(const std::array& value) { 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 }; for (size_t i = 0; i < 16; ++i) { if (value[i] != identity[i]) { return false; } } return true; } uint64_t DefaultSamplerFlags() { // Repeat + linear are the default sampler state in bgfx; no flags needed. return 0; } bgfx::TextureHandle CreateSolidTexture(uint32_t rgba, uint64_t flags) { const bgfx::Memory* mem = bgfx::copy(&rgba, sizeof(rgba)); return bgfx::createTexture2D(1, 1, false, 1, bgfx::TextureFormat::RGBA8, flags, mem); } void SetUniformIfValid(bgfx::UniformHandle handle, const void* data, uint16_t count = 1) { if (bgfx::isValid(handle)) { bgfx::setUniform(handle, data, count); } } bool TryParseRendererType(const std::string& value, bgfx::RendererType::Enum& out) { const std::string lower = ToLower(value); if (lower == "auto") { out = bgfx::RendererType::Count; return true; } if (lower == "vulkan") { out = bgfx::RendererType::Vulkan; return true; } if (lower == "opengl") { out = bgfx::RendererType::OpenGL; return true; } if (lower == "opengles" || lower == "opengles2") { out = bgfx::RendererType::OpenGLES; return true; } if (lower == "direct3d11" || lower == "d3d11") { out = bgfx::RendererType::Direct3D11; return true; } if (lower == "direct3d12" || lower == "d3d12") { out = bgfx::RendererType::Direct3D12; return true; } if (lower == "metal") { out = bgfx::RendererType::Metal; return true; } if (lower == "noop") { out = bgfx::RendererType::Noop; return true; } return false; } std::string RendererTypeName(bgfx::RendererType::Enum type) { if (type == bgfx::RendererType::Count) { return "auto"; } const char* name = bgfx::getRendererName(type); if (!name || name[0] == '\0') { return "unknown"; } return name; } std::vector GetSupportedRenderers() { const uint8_t count = bgfx::getSupportedRenderers(); std::vector renderers; renderers.resize(count); if (count > 0) { bgfx::getSupportedRenderers(count, renderers.data()); } return renderers; } std::string JoinRendererNames(const std::vector& renderers) { if (renderers.empty()) { return "none"; } std::string result; for (size_t i = 0; i < renderers.size(); ++i) { if (i > 0) { result += ", "; } result += RendererTypeName(renderers[i]); } return result; } const char* HandleTypeName(bgfx::NativeWindowHandleType::Enum type) { switch (type) { case bgfx::NativeWindowHandleType::Default: return "default"; case bgfx::NativeWindowHandleType::Wayland: return "wayland"; case bgfx::NativeWindowHandleType::Count: return "count"; default: return "unknown"; } } const char* RendererConfigName(bgfx::RendererType::Enum type) { switch (type) { case bgfx::RendererType::Vulkan: return "vulkan"; case bgfx::RendererType::OpenGL: return "opengl"; case bgfx::RendererType::OpenGLES: return "opengles"; case bgfx::RendererType::Direct3D11: return "direct3d11"; case bgfx::RendererType::Direct3D12: return "direct3d12"; case bgfx::RendererType::Metal: return "metal"; case bgfx::RendererType::Noop: return "noop"; case bgfx::RendererType::Count: default: return "auto"; } } bool IsNoopRenderer(bgfx::RendererType::Enum type) { return type == bgfx::RendererType::Noop; } bool ContainsRenderer(const std::vector& renderers, bgfx::RendererType::Enum type) { return std::find(renderers.begin(), renderers.end(), type) != renderers.end(); } bool IsRendererAllowedOnPlatform(bgfx::RendererType::Enum type, const std::string& platformName) { if (type == bgfx::RendererType::Count || type == bgfx::RendererType::Noop) { return true; } const std::string platformLower = ToLower(platformName); if (platformLower.find("windows") != std::string::npos) { return type == bgfx::RendererType::Direct3D11 || type == bgfx::RendererType::Direct3D12 || type == bgfx::RendererType::Vulkan || type == bgfx::RendererType::OpenGL || type == bgfx::RendererType::OpenGLES; } if (platformLower.find("mac") != std::string::npos || platformLower.find("darwin") != std::string::npos) { return type == bgfx::RendererType::Metal || type == bgfx::RendererType::OpenGL || type == bgfx::RendererType::Vulkan; } return type == bgfx::RendererType::Vulkan || type == bgfx::RendererType::OpenGL || type == bgfx::RendererType::OpenGLES; } void AddRendererIfSupported(std::vector& ordered, const std::vector& supported, bgfx::RendererType::Enum type) { if (IsNoopRenderer(type) || type == bgfx::RendererType::Count) { return; } if (!ContainsRenderer(supported, type)) { return; } if (!ContainsRenderer(ordered, type)) { ordered.push_back(type); } } std::vector BuildPreferredRenderers( const std::vector& supportedRenderers, const std::string& platformName, const std::string& videoDriverName) { std::vector preferred; const std::string platformLower = ToLower(platformName); const std::string videoLower = ToLower(videoDriverName); if (platformLower.find("windows") != std::string::npos) { AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::Direct3D12); AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::Direct3D11); AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::Vulkan); AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::OpenGL); } else if (platformLower.find("mac") != std::string::npos || platformLower.find("darwin") != std::string::npos) { AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::Metal); AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::OpenGL); AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::Vulkan); } else { const bool waylandOrX11 = (videoLower == "wayland") || (videoLower == "x11"); const bool kmsdrm = (videoLower == "kmsdrm"); if (waylandOrX11 || kmsdrm) { AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::Vulkan); AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::OpenGL); AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::OpenGLES); } else { AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::Vulkan); AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::OpenGL); AddRendererIfSupported(preferred, supportedRenderers, bgfx::RendererType::OpenGLES); } } return preferred; } std::optional RecommendFallbackRenderer( bgfx::RendererType::Enum requested, const std::vector& supportedRenderers, const std::string& platformName, const std::string& videoDriverName) { const auto preferred = BuildPreferredRenderers(supportedRenderers, platformName, videoDriverName); for (bgfx::RendererType::Enum type : preferred) { if (type != requested) { return type; } } return std::nullopt; } } // namespace BgfxGraphicsBackend::BgfxGraphicsBackend(std::shared_ptr configService, std::shared_ptr platformService, std::shared_ptr logger, std::shared_ptr pipelineCompiler) : configService_(std::move(configService)), platformService_(std::move(platformService)), logger_(std::move(logger)), pipelineCompiler_(std::move(pipelineCompiler)) { if (logger_) { logger_->Trace("BgfxGraphicsBackend", "BgfxGraphicsBackend", "configService=" + std::string(configService_ ? "set" : "null") + ", platformService=" + std::string(platformService_ ? "set" : "null")); } vertexLayout_.begin() .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) .add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float) .add(bgfx::Attrib::Tangent, 3, bgfx::AttribType::Float) .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) .add(bgfx::Attrib::Color0, 3, bgfx::AttribType::Float) .end(); const std::array identity = { 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 }; viewState_.view = identity; viewState_.proj = identity; viewState_.viewProj = identity; viewState_.cameraPosition = {0.0f, 0.0f, 0.0f}; } BgfxGraphicsBackend::~BgfxGraphicsBackend() { if (logger_) { logger_->Trace("BgfxGraphicsBackend", "~BgfxGraphicsBackend"); } if (initialized_) { Shutdown(); } } void BgfxGraphicsBackend::SetupPlatformData(void* window) { platformData_ = bgfx::PlatformData{}; auto& pd = platformData_; platformHandleInfo_ = PlatformHandleInfo{}; platformHandleInfo_.handleType = bgfx::NativeWindowHandleType::Default; SDL_Window* sdlWindow = static_cast(window); if (!sdlWindow) { if (logger_) { logger_->Trace("BgfxGraphicsBackend", "SetupPlatformData", "windowIsNull=true"); } platformHandleInfo_.hasWindowHandle = false; platformHandleInfo_.hasDisplayHandle = false; 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); void* x11Display = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr); Sint64 x11Window = SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0); const bool hasWayland = wlDisplay && wlSurface; const bool hasX11 = x11Display && x11Window != 0; platformHandleInfo_.hasWayland = hasWayland; platformHandleInfo_.hasX11 = hasX11; if (logger_) { logger_->Trace("BgfxGraphicsBackend", "SetupPlatformData", "waylandAvailable=" + std::string(hasWayland ? "true" : "false") + ", x11Available=" + std::string(hasX11 ? "true" : "false")); } if (hasWayland) { pd.ndt = wlDisplay; pd.nwh = wlSurface; pd.type = bgfx::NativeWindowHandleType::Wayland; platformHandleInfo_.handleType = bgfx::NativeWindowHandleType::Wayland; if (logger_) { logger_->Trace("BgfxGraphicsBackend", "SetupPlatformData", "selectedHandleType=Wayland"); } } else if (hasX11) { pd.ndt = x11Display; pd.nwh = reinterpret_cast(static_cast(x11Window)); pd.type = bgfx::NativeWindowHandleType::Default; platformHandleInfo_.handleType = bgfx::NativeWindowHandleType::Default; if (logger_) { logger_->Trace("BgfxGraphicsBackend", "SetupPlatformData", "selectedHandleType=X11"); } } #else pd.nwh = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); #endif platformHandleInfo_.hasWindowHandle = pd.nwh != nullptr; platformHandleInfo_.hasDisplayHandle = pd.ndt != nullptr; if (logger_) { logger_->Trace("BgfxGraphicsBackend", "SetupPlatformData", "nwh=" + std::to_string(reinterpret_cast(pd.nwh)) + ", ndt=" + std::to_string(reinterpret_cast(pd.ndt)) + ", handleType=" + std::string(HandleTypeName(platformHandleInfo_.handleType))); } bgfx::setPlatformData(pd); } bgfx::RendererType::Enum BgfxGraphicsBackend::ResolveRendererType() const { if (!configService_) { return bgfx::RendererType::Vulkan; } const auto& config = configService_->GetBgfxConfig(); bgfx::RendererType::Enum resolved = bgfx::RendererType::Vulkan; const bool parsed = TryParseRendererType(config.renderer, resolved); if (logger_) { logger_->Trace("BgfxGraphicsBackend", "ResolveRendererType", "renderer=" + config.renderer + ", resolved=" + RendererTypeName(resolved) + ", parsed=" + std::string(parsed ? "true" : "false")); } if (!parsed && logger_) { logger_->Warn("BgfxGraphicsBackend::ResolveRendererType: Unknown renderer '" + config.renderer + "', defaulting to Vulkan"); } return resolved; } void BgfxGraphicsBackend::LogRendererFailureDetails( bgfx::RendererType::Enum renderer, const std::vector& supportedRenderers, const std::string& platformName, const std::string& videoDriverName) { if (!logger_) { return; } logger_->Warn("BgfxGraphicsBackend::Initialize: Renderer " + RendererTypeName(renderer) + " failed (platform=" + platformName + ", videoDriver=" + videoDriverName + ", handleType=" + std::string(HandleTypeName(platformHandleInfo_.handleType)) + ", windowHandle=" + std::string(platformHandleInfo_.hasWindowHandle ? "set" : "null") + ", displayHandle=" + std::string(platformHandleInfo_.hasDisplayHandle ? "set" : "null") + ", supportedRenderers=" + JoinRendererNames(supportedRenderers) + ")"); const auto fallback = RecommendFallbackRenderer(renderer, supportedRenderers, platformName, videoDriverName); if (fallback.has_value()) { logger_->Warn("BgfxGraphicsBackend::Initialize: Recommended fallback renderer=" + RendererTypeName(fallback.value()) + " (set bgfx.renderer=" + std::string(RendererConfigName(fallback.value())) + ")"); } if (renderer == bgfx::RendererType::Vulkan) { if (!platformHandleInfo_.hasWindowHandle) { logger_->Warn("BgfxGraphicsBackend::Initialize: Vulkan requires a native window handle"); } if (videoDriverName == "wayland" && !platformHandleInfo_.hasWayland) { logger_->Warn("BgfxGraphicsBackend::Initialize: SDL reports Wayland, but no Wayland " "window handle was provided"); } if (videoDriverName == "x11" && !platformHandleInfo_.hasX11) { logger_->Warn("BgfxGraphicsBackend::Initialize: SDL reports X11, but no X11 " "window handle was provided"); } } if (platformService_ && !loggedInitFailureDiagnostics_) { platformService_->LogSystemInfo(); loggedInitFailureDiagnostics_ = true; } } void BgfxGraphicsBackend::Initialize(void* window, const GraphicsConfig& config) { if (logger_) { logger_->Trace("BgfxGraphicsBackend", "Initialize"); } if (initialized_) { return; } frameCount_ = 0; (void)config; 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); const auto requestedRenderer = ResolveRendererType(); const auto supportedRenderers = GetSupportedRenderers(); const std::string platformName = platformService_ ? platformService_->GetPlatformName() : "unknown"; const std::string videoDriverName = platformService_ ? platformService_->GetCurrentVideoDriverName() : "unknown"; if (logger_) { logger_->Trace("BgfxGraphicsBackend", "Initialize", "requestedRenderer=" + RendererTypeName(requestedRenderer) + ", supportedRenderers=" + JoinRendererNames(supportedRenderers) + ", platform=" + platformName + ", videoDriver=" + videoDriverName); } if (requestedRenderer != bgfx::RendererType::Count && !ContainsRenderer(supportedRenderers, requestedRenderer) && logger_) { logger_->Warn("BgfxGraphicsBackend::Initialize: Requested renderer=" + RendererTypeName(requestedRenderer) + " is not in the supported renderers list"); } if (requestedRenderer != bgfx::RendererType::Count && !IsRendererAllowedOnPlatform(requestedRenderer, platformName) && logger_) { logger_->Warn("BgfxGraphicsBackend::Initialize: Requested renderer=" + RendererTypeName(requestedRenderer) + " is not recommended for platform=" + platformName); } bgfx::Init init{}; init.resolution.width = viewportWidth_; init.resolution.height = viewportHeight_; init.resolution.reset = BGFX_RESET_VSYNC; init.platformData = platformData_; init.debug = BGFX_DEBUG_TEXT; // Enable debugging for shader validation if (logger_) { logger_->Trace("BgfxGraphicsBackend", "Initialize", "initPlatformData.nwh=" + std::to_string(reinterpret_cast(init.platformData.nwh)) + ", initPlatformData.ndt=" + std::to_string(reinterpret_cast(init.platformData.ndt)) + ", initPlatformData.type=" + std::string(HandleTypeName(init.platformData.type))); } std::vector candidates; auto addCandidate = [&candidates](bgfx::RendererType::Enum type) { if (std::find(candidates.begin(), candidates.end(), type) == candidates.end()) { candidates.push_back(type); } }; const auto preferredRenderers = BuildPreferredRenderers(supportedRenderers, platformName, videoDriverName); if (logger_) { logger_->Trace("BgfxGraphicsBackend", "Initialize", "preferredRenderers=" + JoinRendererNames(preferredRenderers)); } if (requestedRenderer != bgfx::RendererType::Count) { addCandidate(requestedRenderer); } for (bgfx::RendererType::Enum renderer : preferredRenderers) { addCandidate(renderer); } for (bgfx::RendererType::Enum renderer : supportedRenderers) { if (!IsNoopRenderer(renderer) && renderer != bgfx::RendererType::Count && IsRendererAllowedOnPlatform(renderer, platformName)) { addCandidate(renderer); } } addCandidate(bgfx::RendererType::Count); if (ContainsRenderer(supportedRenderers, bgfx::RendererType::Noop) || requestedRenderer == bgfx::RendererType::Noop) { addCandidate(bgfx::RendererType::Noop); } if (logger_) { logger_->Trace("BgfxGraphicsBackend", "Initialize", "candidateRenderers=" + JoinRendererNames(candidates)); } bool initialized = false; bool requestedFailed = false; const bool requestedExplicit = requestedRenderer != bgfx::RendererType::Count; for (bgfx::RendererType::Enum renderer : candidates) { init.type = renderer; if (logger_) { logger_->Trace("BgfxGraphicsBackend", "Initialize", "attemptingRenderer=" + RendererTypeName(renderer)); } if (bgfx::init(init)) { initialized = true; break; } if (logger_) { logger_->Warn("BgfxGraphicsBackend::Initialize: bgfx init failed for renderer=" + RendererTypeName(renderer)); } if (renderer == requestedRenderer && !requestedFailed) { requestedFailed = true; LogRendererFailureDetails(renderer, supportedRenderers, platformName, videoDriverName); } } if (!initialized) { if (platformService_ && !loggedInitFailureDiagnostics_) { platformService_->LogSystemInfo(); loggedInitFailureDiagnostics_ = true; } throw std::runtime_error("Failed to initialize bgfx"); } if (logger_) { logger_->Trace("BgfxGraphicsBackend", "Initialize", "selectedRenderer=" + RendererTypeName(bgfx::getRendererType())); } if (requestedExplicit && bgfx::getRendererType() != requestedRenderer && logger_) { logger_->Warn("BgfxGraphicsBackend::Initialize: Requested renderer=" + RendererTypeName(requestedRenderer) + ", using=" + RendererTypeName(bgfx::getRendererType())); } if (bgfx::getRendererType() == bgfx::RendererType::Noop && logger_) { logger_->Warn("BgfxGraphicsBackend::Initialize: Noop renderer selected; rendering disabled"); } bgfx::setViewClear(viewId_, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x1f1f1fff, 1.0f, 0); bgfx::setDebug(BGFX_DEBUG_TEXT); InitializeUniforms(); initialized_ = true; } void BgfxGraphicsBackend::Shutdown() { if (logger_) { logger_->Trace("BgfxGraphicsBackend", "Shutdown"); } if (!initialized_) { return; } DestroyPipelines(); DestroyBuffers(); DestroyUniforms(); bgfx::shutdown(); initialized_ = false; frameCount_ = 0; } 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 { const bgfx::RendererType::Enum rendererType = bgfx::getRendererType(); if (logger_) { logger_->Trace("BgfxGraphicsBackend", "CreateShader", "label=" + label + ", renderer=" + RendererTypeName(rendererType) + ", sourceLength=" + std::to_string(source.size()) + ", compiler=BgfxShaderCompiler"); } BgfxShaderCompiler compiler(logger_, pipelineCompiler_); bgfx::ShaderHandle handle = compiler.CompileShader(label, source, isVertex, {}, {}); if (!bgfx::isValid(handle)) { if (logger_) { logger_->Error("BgfxGraphicsBackend::CreateShader failed for " + label + " (renderer=" + RendererTypeName(rendererType) + ")"); } throw std::runtime_error("BgfxGraphicsBackend::CreateShader failed for " + label); } return handle; } bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string& path, uint64_t samplerFlags) const { if (logger_) { logger_->Trace("BgfxGraphicsBackend", "LoadTextureFromFile", "path=" + path); } if (!HasProcessedFrame()) { if (logger_) { logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: Attempted to load texture BEFORE first " "bgfx::frame(). Call BeginFrame()+EndFrame() before loading textures. path=" + path); } return BGFX_INVALID_HANDLE; } int width = 0; int height = 0; int channels = 0; stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, STBI_rgb_alpha); if (!pixels || width <= 0 || height <= 0) { if (logger_) { logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: failed to load " + path + " reason=" + (stbi_failure_reason() ? stbi_failure_reason() : "unknown")); } if (pixels) { stbi_image_free(pixels); } return BGFX_INVALID_HANDLE; } // Validate texture dimensions against GPU capabilities const bgfx::Caps* caps = bgfx::getCaps(); if (caps) { const uint16_t maxTextureSize = caps->limits.maxTextureSize; if (width > maxTextureSize || height > maxTextureSize) { if (logger_) { logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: texture " + path + " size (" + std::to_string(width) + "x" + std::to_string(height) + ") exceeds GPU max texture size (" + std::to_string(maxTextureSize) + ")"); } stbi_image_free(pixels); return BGFX_INVALID_HANDLE; } } const uint32_t size = static_cast(width * height * 4); // Check memory budget before allocation if (!textureMemoryTracker_.CanAllocate(size)) { if (logger_) { logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: texture memory budget exceeded for " + path + " - requested " + std::to_string(size / 1024 / 1024) + " MB" + ", used " + std::to_string(textureMemoryTracker_.GetUsedBytes() / 1024 / 1024) + " MB" + " / " + std::to_string(textureMemoryTracker_.GetMaxBytes() / 1024 / 1024) + " MB"); } stbi_image_free(pixels); return BGFX_INVALID_HANDLE; } const bgfx::Memory* mem = bgfx::copy(pixels, size); stbi_image_free(pixels); // Validate bgfx::copy() succeeded if (!mem) { if (logger_) { logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: bgfx::copy() failed for " + path + " - likely out of GPU memory (attempted to allocate " + std::to_string(size / 1024 / 1024) + " MB)"); } return BGFX_INVALID_HANDLE; } bgfx::TextureHandle handle = bgfx::createTexture2D( static_cast(width), static_cast(height), false, 1, bgfx::TextureFormat::RGBA8, samplerFlags, mem); if (!bgfx::isValid(handle)) { if (logger_) { logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: createTexture2D failed for " + path + " (" + std::to_string(width) + "x" + std::to_string(height) + " = " + std::to_string(size / 1024 / 1024) + " MB) - GPU resource exhaustion likely"); } return BGFX_INVALID_HANDLE; } if (logger_) { logger_->Trace("BgfxGraphicsBackend", "LoadTextureFromFile", "path=" + path + ", width=" + std::to_string(width) + ", height=" + std::to_string(height) + ", memoryMB=" + std::to_string(size / 1024 / 1024)); } return handle; } void BgfxGraphicsBackend::InitializeUniforms() { materialXUniforms_.worldMatrix = bgfx::createUniform("u_worldMatrix", bgfx::UniformType::Mat4); materialXUniforms_.viewMatrix = bgfx::createUniform("u_viewMatrix", bgfx::UniformType::Mat4); materialXUniforms_.projectionMatrix = bgfx::createUniform("u_projectionMatrix", bgfx::UniformType::Mat4); materialXUniforms_.viewProjectionMatrix = bgfx::createUniform("u_viewProjectionMatrix", bgfx::UniformType::Mat4); materialXUniforms_.worldViewMatrix = bgfx::createUniform("u_worldViewMatrix", bgfx::UniformType::Mat4); materialXUniforms_.worldViewProjectionMatrix = bgfx::createUniform("u_worldViewProjectionMatrix", bgfx::UniformType::Mat4); materialXUniforms_.worldInverseTransposeMatrix = bgfx::createUniform("u_worldInverseTransposeMatrix", bgfx::UniformType::Mat4); materialXUniforms_.viewPosition = bgfx::createUniform("u_viewPosition", bgfx::UniformType::Vec4); } void BgfxGraphicsBackend::DestroyUniforms() { bgfx::UniformHandle handles[] = { materialXUniforms_.worldMatrix, materialXUniforms_.viewMatrix, materialXUniforms_.projectionMatrix, materialXUniforms_.viewProjectionMatrix, materialXUniforms_.worldViewMatrix, materialXUniforms_.worldViewProjectionMatrix, materialXUniforms_.worldInverseTransposeMatrix, materialXUniforms_.viewPosition }; for (bgfx::UniformHandle handle : handles) { if (bgfx::isValid(handle)) { bgfx::destroy(handle); } } materialXUniforms_ = MaterialXUniforms{}; } void BgfxGraphicsBackend::ApplyMaterialXUniforms(const std::array& modelMatrix) { glm::mat4 model = ToMat4(modelMatrix); glm::mat4 view = ToMat4(viewState_.view); glm::mat4 proj = ToMat4(viewState_.proj); glm::mat4 viewProj = (IsIdentityMatrix(viewState_.view) && IsIdentityMatrix(viewState_.proj)) ? ToMat4(viewState_.viewProj) : proj * view; glm::mat4 worldView = view * model; glm::mat4 worldViewProj = viewProj * model; glm::mat4 worldInverseTranspose = glm::transpose(glm::inverse(model)); SetUniformIfValid(materialXUniforms_.worldMatrix, glm::value_ptr(model)); SetUniformIfValid(materialXUniforms_.viewMatrix, glm::value_ptr(view)); SetUniformIfValid(materialXUniforms_.projectionMatrix, glm::value_ptr(proj)); SetUniformIfValid(materialXUniforms_.viewProjectionMatrix, glm::value_ptr(viewProj)); SetUniformIfValid(materialXUniforms_.worldViewMatrix, glm::value_ptr(worldView)); SetUniformIfValid(materialXUniforms_.worldViewProjectionMatrix, glm::value_ptr(worldViewProj)); SetUniformIfValid(materialXUniforms_.worldInverseTransposeMatrix, glm::value_ptr(worldInverseTranspose)); float viewPosition[4] = { viewState_.cameraPosition[0], viewState_.cameraPosition[1], viewState_.cameraPosition[2], 1.0f }; SetUniformIfValid(materialXUniforms_.viewPosition, viewPosition); } 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; if (!shaderPaths.textures.empty()) { const uint64_t samplerFlags = DefaultSamplerFlags(); uint8_t stage = 0; const bgfx::Caps* caps = bgfx::getCaps(); uint8_t maxStages = 0; if (caps) { maxStages = static_cast(std::min(caps->limits.maxTextureSamplers, 255u)); } if (logger_) { logger_->Trace("BgfxGraphicsBackend", "CreatePipeline", "maxTextureSamplers=" + std::to_string(maxStages)); } if (maxStages == 0) { if (logger_) { logger_->Warn("BgfxGraphicsBackend::CreatePipeline: maxTextureSamplers unavailable for " + shaderKey); } } else { for (const auto& texture : shaderPaths.textures) { if (stage >= maxStages) { if (logger_) { logger_->Warn("BgfxGraphicsBackend::CreatePipeline: texture limit reached for " + shaderKey); } break; } if (texture.uniformName.empty() || texture.path.empty()) { continue; } PipelineEntry::TextureBinding binding{}; binding.stage = stage; binding.uniformName = texture.uniformName; binding.sourcePath = texture.path; binding.sampler = bgfx::createUniform(binding.uniformName.c_str(), bgfx::UniformType::Sampler); // Validate sampler creation if (!bgfx::isValid(binding.sampler)) { if (logger_) { logger_->Error("BgfxGraphicsBackend::CreatePipeline: failed to create sampler uniform '" + binding.uniformName + "' for " + shaderKey); } continue; // Skip this texture binding } // Try to load texture from file binding.texture = LoadTextureFromFile(binding.sourcePath, samplerFlags); if (bgfx::isValid(binding.texture)) { // Estimate texture memory size (assume RGBA8 format, no mipmaps for now) // In production, should query actual texture info from bgfx // For now, estimate based on typical 2048x2048 textures binding.memorySizeBytes = 2048 * 2048 * 4; // Conservative estimate textureMemoryTracker_.Allocate(binding.memorySizeBytes); } else { if (logger_) { logger_->Warn("BgfxGraphicsBackend::CreatePipeline: texture load failed for " + binding.sourcePath + ", creating fallback texture"); } // Use fallback magenta texture (1x1) binding.texture = CreateSolidTexture(0xff00ffff, samplerFlags); if (bgfx::isValid(binding.texture)) { binding.memorySizeBytes = 1 * 1 * 4; // 1x1 RGBA8 textureMemoryTracker_.Allocate(binding.memorySizeBytes); } } // Validate texture creation succeeded (either main or fallback) if (!bgfx::isValid(binding.texture)) { if (logger_) { logger_->Error("BgfxGraphicsBackend::CreatePipeline: both texture load AND fallback failed for " + shaderKey + " - skipping texture binding '" + binding.uniformName + "'"); } // Cleanup the sampler we created if (bgfx::isValid(binding.sampler)) { bgfx::destroy(binding.sampler); } continue; // Skip this texture binding entirely } if (logger_) { logger_->Trace("BgfxGraphicsBackend", "CreatePipeline", "shaderKey=" + shaderKey + ", textureUniform=" + binding.uniformName + ", texturePath=" + binding.sourcePath + ", stage=" + std::to_string(binding.stage)); } // Successfully created texture binding - increment stage and add to pipeline stage++; entry->textures.push_back(std::move(binding)); } } } 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; } for (const auto& binding : it->second->textures) { if (bgfx::isValid(binding.texture)) { bgfx::destroy(binding.texture); // Free texture memory from budget if (binding.memorySizeBytes > 0) { textureMemoryTracker_.Free(binding.memorySizeBytes); } } if (bgfx::isValid(binding.sampler)) { bgfx::destroy(binding.sampler); } } 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; } bgfx::setViewRect(viewId_, 0, 0, viewportWidth_, viewportHeight_); bgfx::touch(viewId_); return true; } bool BgfxGraphicsBackend::EndFrame(GraphicsDeviceHandle device) { if (!initialized_) { return false; } const uint32_t frameNumber = bgfx::frame(); frameCount_ = frameNumber + 1; if (logger_) { logger_->Trace("BgfxGraphicsBackend", "EndFrame", "frameNumber=" + std::to_string(frameNumber)); } return true; } void BgfxGraphicsBackend::SetViewState(const ViewState& viewState) { viewState_ = viewState; bgfx::setViewTransform(viewId_, viewState_.view.data(), viewState_.proj.data()); } 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; if (logger_) { logger_->Trace("BgfxGraphicsBackend", "Draw", "vertexOffset=" + std::to_string(vertexOffset) + ", indexOffset=" + std::to_string(indexOffset) + ", indexCount=" + std::to_string(indexCount) + ", totalVertices=" + std::to_string(vb->vertexCount)); } // When using indexed rendering with a vertex offset, bgfx expects: // - setVertexBuffer: (handle, startVertex=0, numVertices=all) // - setIndexBuffer: (handle, firstIndex, numIndices) // The indices in the index buffer are already adjusted to reference the correct vertices // in the combined vertex buffer, so we should NOT apply vertexOffset again here. // Using the full vertex buffer ensures all vertex data is accessible. // NOTE: Do NOT call bgfx::setTransform() when using MaterialX shaders with explicit uniforms. // MaterialX shaders receive transformation matrices via explicit uniforms (u_worldMatrix, // u_worldViewProjectionMatrix, etc.) set in ApplyMaterialXUniforms(). Calling setTransform() // causes conflicts, especially with Vulkan, resulting in garbage/rainbow artifacts. ApplyMaterialXUniforms(modelMatrix); for (const auto& binding : pipelineIt->second->textures) { if (bgfx::isValid(binding.sampler) && bgfx::isValid(binding.texture)) { bgfx::setTexture(binding.stage, binding.sampler, binding.texture); } } bgfx::setVertexBuffer(0, vb->handle); 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_) { for (const auto& binding : entry->textures) { if (bgfx::isValid(binding.texture)) { bgfx::destroy(binding.texture); // Free texture memory from budget if (binding.memorySizeBytes > 0) { textureMemoryTracker_.Free(binding.memorySizeBytes); } } if (bgfx::isValid(binding.sampler)) { bgfx::destroy(binding.sampler); } } 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