mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
1195 lines
42 KiB
C++
1195 lines
42 KiB
C++
#include "bgfx_gui_service.hpp"
|
|
|
|
#include "../interfaces/config_types.hpp"
|
|
#include "../interfaces/gui_types.hpp"
|
|
|
|
#include <bgfx/bgfx.h>
|
|
#include <bx/math.h>
|
|
#include <shaderc/shaderc.hpp>
|
|
|
|
#include <ft2build.h>
|
|
#include FT_FREETYPE_H
|
|
|
|
#include <lunasvg/lunasvg.h>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <numeric>
|
|
#include <optional>
|
|
|
|
namespace sdl3cpp::services::impl {
|
|
|
|
namespace {
|
|
|
|
constexpr uint64_t kGuiSamplerFlags = BGFX_SAMPLER_U_CLAMP |
|
|
BGFX_SAMPLER_V_CLAMP;
|
|
constexpr uint8_t kUniformFragmentBit = 0x10;
|
|
constexpr uint8_t kUniformMask = 0
|
|
| 0x10
|
|
| 0x20
|
|
| 0x40
|
|
| 0x80;
|
|
|
|
struct GuiShaderUniform {
|
|
std::string name;
|
|
bgfx::UniformType::Enum type = bgfx::UniformType::Count;
|
|
uint8_t num = 0;
|
|
uint16_t regIndex = 0;
|
|
uint16_t regCount = 0;
|
|
uint8_t texComponent = 0;
|
|
uint8_t texDimension = 0;
|
|
uint16_t texFormat = 0;
|
|
};
|
|
|
|
uint16_t WriteUniformArray(uint8_t* data,
|
|
uint32_t& offset,
|
|
const std::vector<GuiShaderUniform>& uniforms,
|
|
bool isFragmentShader) {
|
|
uint16_t size = 0;
|
|
const uint16_t count = static_cast<uint16_t>(uniforms.size());
|
|
std::memcpy(data + offset, &count, sizeof(count));
|
|
offset += sizeof(count);
|
|
|
|
const uint8_t fragmentBit = isFragmentShader ? kUniformFragmentBit : 0;
|
|
for (const auto& un : uniforms) {
|
|
if ((static_cast<uint8_t>(un.type) & ~kUniformMask) > bgfx::UniformType::End) {
|
|
size = std::max<uint16_t>(size, static_cast<uint16_t>(un.regIndex + un.regCount * 16));
|
|
}
|
|
|
|
const uint8_t nameSize = static_cast<uint8_t>(un.name.size());
|
|
std::memcpy(data + offset, &nameSize, sizeof(nameSize));
|
|
offset += sizeof(nameSize);
|
|
std::memcpy(data + offset, un.name.data(), nameSize);
|
|
offset += nameSize;
|
|
|
|
const uint8_t typeByte = static_cast<uint8_t>(un.type) | fragmentBit;
|
|
std::memcpy(data + offset, &typeByte, sizeof(typeByte));
|
|
offset += sizeof(typeByte);
|
|
std::memcpy(data + offset, &un.num, sizeof(un.num));
|
|
offset += sizeof(un.num);
|
|
std::memcpy(data + offset, &un.regIndex, sizeof(un.regIndex));
|
|
offset += sizeof(un.regIndex);
|
|
std::memcpy(data + offset, &un.regCount, sizeof(un.regCount));
|
|
offset += sizeof(un.regCount);
|
|
std::memcpy(data + offset, &un.texComponent, sizeof(un.texComponent));
|
|
offset += sizeof(un.texComponent);
|
|
std::memcpy(data + offset, &un.texDimension, sizeof(un.texDimension));
|
|
offset += sizeof(un.texDimension);
|
|
std::memcpy(data + offset, &un.texFormat, sizeof(un.texFormat));
|
|
offset += sizeof(un.texFormat);
|
|
}
|
|
return size;
|
|
}
|
|
|
|
uint16_t GuiAttribToId(bgfx::Attrib::Enum attr) {
|
|
switch (attr) {
|
|
case bgfx::Attrib::Position:
|
|
return 0x0001;
|
|
case bgfx::Attrib::Color0:
|
|
return 0x0005;
|
|
case bgfx::Attrib::TexCoord0:
|
|
return 0x0010;
|
|
default:
|
|
return UINT16_MAX;
|
|
}
|
|
}
|
|
|
|
const char* RendererTypeName(bgfx::RendererType::Enum type) {
|
|
switch (type) {
|
|
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:
|
|
return "Auto";
|
|
default:
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
const char* kGuiVertexSource = R"(
|
|
#version 450
|
|
|
|
layout(location = 0) in vec3 inPos;
|
|
layout(location = 1) in vec4 inColor;
|
|
layout(location = 2) in vec2 inTexCoord;
|
|
|
|
layout(location = 0) out vec4 fragColor;
|
|
layout(location = 1) out vec2 fragTexCoord;
|
|
|
|
layout(std140) uniform GuiUniforms {
|
|
mat4 u_modelViewProj;
|
|
};
|
|
|
|
void main() {
|
|
fragColor = inColor;
|
|
fragTexCoord = inTexCoord;
|
|
gl_Position = u_modelViewProj * vec4(inPos, 1.0);
|
|
}
|
|
)";
|
|
|
|
const char* kGuiFragmentSource = R"(
|
|
#version 450
|
|
|
|
layout(location = 0) in vec4 fragColor;
|
|
layout(location = 1) in vec2 fragTexCoord;
|
|
|
|
layout(location = 0) out vec4 outColor;
|
|
|
|
uniform sampler2D s_tex;
|
|
|
|
void main() {
|
|
outColor = fragColor * texture(s_tex, fragTexCoord);
|
|
}
|
|
)";
|
|
|
|
std::string ToLower(std::string value) {
|
|
std::transform(value.begin(), value.end(), value.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
return value;
|
|
}
|
|
|
|
float Clamp01(float value) {
|
|
return std::clamp(value, 0.0f, 1.0f);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
struct BgfxGuiService::FreeTypeState {
|
|
FT_Library library = nullptr;
|
|
FT_Face face = nullptr;
|
|
std::filesystem::path fontPath;
|
|
bool ready = false;
|
|
};
|
|
|
|
BgfxGuiService::BgfxGuiService(std::shared_ptr<IConfigService> configService,
|
|
std::shared_ptr<ILogger> logger)
|
|
: configService_(std::move(configService)),
|
|
logger_(std::move(logger)),
|
|
materialxGenerator_(logger_),
|
|
freeType_(std::make_unique<FreeTypeState>()) {
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "BgfxGuiService",
|
|
"configService=" + std::string(configService_ ? "set" : "null"));
|
|
}
|
|
}
|
|
|
|
BgfxGuiService::~BgfxGuiService() {
|
|
if (initialized_) {
|
|
Shutdown();
|
|
}
|
|
}
|
|
|
|
void BgfxGuiService::PrepareFrame(const std::vector<GuiCommand>& commands,
|
|
uint32_t width,
|
|
uint32_t height) {
|
|
if (!initialized_) {
|
|
InitializeResources();
|
|
}
|
|
|
|
if (!bgfx::isValid(program_) || !bgfx::isValid(whiteTexture_)) {
|
|
if (logger_ && !loggedMissingResources_) {
|
|
logger_->Warn("BgfxGuiService::PrepareFrame: GUI resources not initialized");
|
|
}
|
|
loggedMissingResources_ = true;
|
|
return;
|
|
}
|
|
if (loggedMissingResources_ && logger_) {
|
|
logger_->Trace("BgfxGuiService", "PrepareFrame", "GUI resources recovered");
|
|
}
|
|
loggedMissingResources_ = false;
|
|
|
|
ApplyGuiView(width, height);
|
|
scissorStack_.clear();
|
|
++frameIndex_;
|
|
|
|
for (const auto& command : commands) {
|
|
switch (command.type) {
|
|
case GuiCommand::Type::ClipPush: {
|
|
ScissorRect incoming{command.rect.x, command.rect.y, command.rect.width, command.rect.height};
|
|
auto current = CurrentScissor();
|
|
if (current) {
|
|
auto merged = IntersectScissor(*current, incoming);
|
|
if (merged) {
|
|
scissorStack_.push_back(*merged);
|
|
} else {
|
|
scissorStack_.push_back(ScissorRect{0.0f, 0.0f, 0.0f, 0.0f});
|
|
}
|
|
} else {
|
|
scissorStack_.push_back(incoming);
|
|
}
|
|
break;
|
|
}
|
|
case GuiCommand::Type::ClipPop: {
|
|
if (!scissorStack_.empty()) {
|
|
scissorStack_.pop_back();
|
|
}
|
|
break;
|
|
}
|
|
case GuiCommand::Type::Rect: {
|
|
SubmitRect(command, BuildScissor(std::nullopt));
|
|
break;
|
|
}
|
|
case GuiCommand::Type::Text: {
|
|
std::optional<ScissorRect> scoped;
|
|
if (command.hasClipRect) {
|
|
scoped = ScissorRect{command.clipRect.x, command.clipRect.y,
|
|
command.clipRect.width, command.clipRect.height};
|
|
}
|
|
SubmitText(command, BuildScissor(scoped));
|
|
break;
|
|
}
|
|
case GuiCommand::Type::Svg: {
|
|
SubmitSvg(command, BuildScissor(std::nullopt));
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
PruneTextCache();
|
|
PruneSvgCache();
|
|
}
|
|
|
|
void BgfxGuiService::Shutdown() noexcept {
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "Shutdown");
|
|
}
|
|
|
|
for (auto& [key, entry] : textCache_) {
|
|
if (bgfx::isValid(entry.texture)) {
|
|
bgfx::destroy(entry.texture);
|
|
}
|
|
}
|
|
textCache_.clear();
|
|
|
|
for (auto& [key, entry] : svgCache_) {
|
|
if (bgfx::isValid(entry.texture)) {
|
|
bgfx::destroy(entry.texture);
|
|
}
|
|
}
|
|
svgCache_.clear();
|
|
|
|
if (bgfx::isValid(whiteTexture_)) {
|
|
bgfx::destroy(whiteTexture_);
|
|
whiteTexture_ = BGFX_INVALID_HANDLE;
|
|
}
|
|
if (bgfx::isValid(program_)) {
|
|
bgfx::destroy(program_);
|
|
program_ = BGFX_INVALID_HANDLE;
|
|
}
|
|
if (bgfx::isValid(sampler_)) {
|
|
bgfx::destroy(sampler_);
|
|
sampler_ = BGFX_INVALID_HANDLE;
|
|
}
|
|
if (bgfx::isValid(modelViewProjUniform_)) {
|
|
bgfx::destroy(modelViewProjUniform_);
|
|
modelViewProjUniform_ = BGFX_INVALID_HANDLE;
|
|
}
|
|
|
|
if (freeType_) {
|
|
if (freeType_->face) {
|
|
FT_Done_Face(freeType_->face);
|
|
freeType_->face = nullptr;
|
|
}
|
|
if (freeType_->library) {
|
|
FT_Done_FreeType(freeType_->library);
|
|
freeType_->library = nullptr;
|
|
}
|
|
freeType_->ready = false;
|
|
}
|
|
|
|
initialized_ = false;
|
|
}
|
|
|
|
bool BgfxGuiService::IsProgramReady() const {
|
|
return bgfx::isValid(program_);
|
|
}
|
|
|
|
bool BgfxGuiService::IsWhiteTextureReady() const {
|
|
return bgfx::isValid(whiteTexture_);
|
|
}
|
|
|
|
void BgfxGuiService::InitializeResources() {
|
|
if (initialized_) {
|
|
return;
|
|
}
|
|
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "InitializeResources", "Creating GUI shader uniforms");
|
|
}
|
|
|
|
layout_.begin()
|
|
.add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
|
|
.add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Float)
|
|
.add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float)
|
|
.end();
|
|
|
|
modelViewProjUniform_ = bgfx::createUniform("u_modelViewProj", bgfx::UniformType::Mat4);
|
|
sampler_ = bgfx::createUniform("s_tex", bgfx::UniformType::Sampler);
|
|
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "InitializeResources",
|
|
"Uniforms created: modelViewProj=" + std::to_string(bgfx::isValid(modelViewProjUniform_)) +
|
|
", sampler=" + std::to_string(bgfx::isValid(sampler_)));
|
|
}
|
|
|
|
const char* vertexSource = kGuiVertexSource;
|
|
const char* fragmentSource = kGuiFragmentSource;
|
|
guiVertexSourceOverride_.clear();
|
|
guiFragmentSourceOverride_.clear();
|
|
|
|
if (configService_) {
|
|
const auto& materialConfig = configService_->GetMaterialXConfig();
|
|
if (materialConfig.enabled && materialConfig.shaderKey == "gui") {
|
|
try {
|
|
ShaderPaths generated = materialxGenerator_.Generate(materialConfig, {});
|
|
if (!generated.vertexSource.empty() && !generated.fragmentSource.empty()) {
|
|
guiVertexSourceOverride_ = std::move(generated.vertexSource);
|
|
guiFragmentSourceOverride_ = std::move(generated.fragmentSource);
|
|
vertexSource = guiVertexSourceOverride_.c_str();
|
|
fragmentSource = guiFragmentSourceOverride_.c_str();
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "InitializeResources",
|
|
"Using MaterialX GUI shaders");
|
|
}
|
|
} else if (logger_) {
|
|
logger_->Warn("BgfxGuiService::InitializeResources: MaterialX GUI shaders were empty; falling back");
|
|
}
|
|
} catch (const std::exception& ex) {
|
|
if (logger_) {
|
|
logger_->Warn("BgfxGuiService::InitializeResources: MaterialX GUI shader generation failed: " +
|
|
std::string(ex.what()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
program_ = CreateProgram(vertexSource, fragmentSource);
|
|
|
|
const uint32_t whitePixel = 0xffffffff;
|
|
whiteTexture_ = CreateTexture(reinterpret_cast<const uint8_t*>(&whitePixel), 1, 1, kGuiSamplerFlags);
|
|
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "InitializeResources",
|
|
"Resources created: program=" + std::to_string(bgfx::isValid(program_)) +
|
|
", whiteTexture=" + std::to_string(bgfx::isValid(whiteTexture_)));
|
|
}
|
|
|
|
if (!bgfx::isValid(program_) && logger_) {
|
|
logger_->Error("BgfxGuiService::InitializeResources: Failed to create GUI shader program");
|
|
}
|
|
|
|
EnsureFontReady();
|
|
initialized_ = true;
|
|
}
|
|
|
|
void BgfxGuiService::EnsureFontReady() {
|
|
if (!freeType_ || freeType_->ready) {
|
|
return;
|
|
}
|
|
|
|
GuiFontConfig fontConfig{};
|
|
if (configService_) {
|
|
fontConfig = configService_->GetGuiFontConfig();
|
|
}
|
|
|
|
if (!fontConfig.useFreeType) {
|
|
if (logger_) {
|
|
logger_->Warn("BgfxGuiService::EnsureFontReady: use_freetype disabled; GUI text disabled");
|
|
}
|
|
return;
|
|
}
|
|
|
|
defaultFontSize_ = fontConfig.fontSize > 0.0f
|
|
? static_cast<int>(std::lround(fontConfig.fontSize))
|
|
: defaultFontSize_;
|
|
|
|
std::filesystem::path fontPath = fontConfig.fontPath;
|
|
if (fontPath.empty()) {
|
|
fontPath = ResolveDefaultFontPath();
|
|
}
|
|
fontPath = ResolvePath(fontPath);
|
|
|
|
if (fontPath.empty() || !std::filesystem::exists(fontPath)) {
|
|
if (logger_) {
|
|
logger_->Warn("BgfxGuiService::EnsureFontReady: font path missing; GUI text disabled");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (FT_Init_FreeType(&freeType_->library) != 0) {
|
|
if (logger_) {
|
|
logger_->Error("BgfxGuiService::EnsureFontReady: FreeType init failed");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (FT_New_Face(freeType_->library, fontPath.string().c_str(), 0, &freeType_->face) != 0) {
|
|
if (logger_) {
|
|
logger_->Error("BgfxGuiService::EnsureFontReady: Failed to load font " + fontPath.string());
|
|
}
|
|
FT_Done_FreeType(freeType_->library);
|
|
freeType_->library = nullptr;
|
|
return;
|
|
}
|
|
|
|
freeType_->fontPath = fontPath;
|
|
freeType_->ready = true;
|
|
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "EnsureFontReady",
|
|
"fontPath=" + fontPath.string() +
|
|
", defaultSize=" + std::to_string(defaultFontSize_));
|
|
}
|
|
}
|
|
|
|
void BgfxGuiService::ApplyGuiView(uint32_t width, uint32_t height) {
|
|
const uint32_t previousWidth = frameWidth_;
|
|
const uint32_t previousHeight = frameHeight_;
|
|
frameWidth_ = width;
|
|
frameHeight_ = height;
|
|
|
|
float view[16];
|
|
float proj[16];
|
|
bx::mtxIdentity(view);
|
|
const bool homogeneousDepth = bgfx::getCaps() && bgfx::getCaps()->homogeneousDepth;
|
|
bx::mtxOrtho(proj,
|
|
0.0f,
|
|
static_cast<float>(width),
|
|
static_cast<float>(height),
|
|
0.0f,
|
|
0.0f,
|
|
100.0f,
|
|
0.0f,
|
|
homogeneousDepth);
|
|
std::copy(std::begin(proj), std::end(proj), viewProjection_.begin());
|
|
|
|
if (logger_ && (previousWidth != width || previousHeight != height)) {
|
|
logger_->Trace("BgfxGuiService", "ApplyGuiView",
|
|
"viewport=" + std::to_string(width) + "x" + std::to_string(height));
|
|
logger_->Trace("BgfxGuiService", "ApplyGuiView",
|
|
"projection[0-3]=[" + std::to_string(proj[0]) + "," +
|
|
std::to_string(proj[1]) + "," +
|
|
std::to_string(proj[2]) + "," +
|
|
std::to_string(proj[3]) + "]");
|
|
logger_->Trace("BgfxGuiService", "ApplyGuiView",
|
|
"projection[12-15]=[" + std::to_string(proj[12]) + "," +
|
|
std::to_string(proj[13]) + "," +
|
|
std::to_string(proj[14]) + "," +
|
|
std::to_string(proj[15]) + "]");
|
|
}
|
|
|
|
bgfx::setViewTransform(viewId_, view, proj);
|
|
bgfx::setViewRect(viewId_, 0, 0,
|
|
static_cast<uint16_t>(std::min<uint32_t>(width, 0xffff)),
|
|
static_cast<uint16_t>(std::min<uint32_t>(height, 0xffff)));
|
|
bgfx::touch(viewId_);
|
|
}
|
|
|
|
std::filesystem::path BgfxGuiService::ResolvePath(const std::filesystem::path& path) const {
|
|
if (path.empty() || path.is_absolute()) {
|
|
return path;
|
|
}
|
|
|
|
std::vector<std::filesystem::path> roots;
|
|
if (configService_) {
|
|
auto scriptPath = configService_->GetScriptPath();
|
|
if (!scriptPath.empty()) {
|
|
auto scriptDir = scriptPath.parent_path();
|
|
if (!scriptDir.empty()) {
|
|
roots.push_back(scriptDir);
|
|
auto projectRoot = scriptDir.parent_path();
|
|
if (!projectRoot.empty()) {
|
|
roots.push_back(projectRoot);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
roots.push_back(std::filesystem::current_path());
|
|
|
|
for (const auto& root : roots) {
|
|
auto candidate = root / path;
|
|
if (std::filesystem::exists(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
std::filesystem::path BgfxGuiService::ResolveDefaultFontPath() const {
|
|
std::vector<std::filesystem::path> candidates;
|
|
if (configService_) {
|
|
auto scriptPath = configService_->GetScriptPath();
|
|
if (!scriptPath.empty()) {
|
|
auto scriptDir = scriptPath.parent_path();
|
|
candidates.push_back(scriptDir / "assets" / "fonts" / "Roboto-Regular.ttf");
|
|
candidates.push_back(scriptDir.parent_path() / "scripts" / "assets" / "fonts" / "Roboto-Regular.ttf");
|
|
}
|
|
}
|
|
candidates.push_back(std::filesystem::current_path() / "scripts" / "assets" / "fonts" / "Roboto-Regular.ttf");
|
|
|
|
for (const auto& candidate : candidates) {
|
|
if (!candidate.empty() && std::filesystem::exists(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
std::optional<BgfxGuiService::ScissorRect> BgfxGuiService::IntersectScissor(const ScissorRect& a,
|
|
const ScissorRect& b) const {
|
|
float x0 = std::max(a.x, b.x);
|
|
float y0 = std::max(a.y, b.y);
|
|
float x1 = std::min(a.x + a.width, b.x + b.width);
|
|
float y1 = std::min(a.y + a.height, b.y + b.height);
|
|
if (x1 <= x0 || y1 <= y0) {
|
|
return std::nullopt;
|
|
}
|
|
return ScissorRect{x0, y0, x1 - x0, y1 - y0};
|
|
}
|
|
|
|
std::optional<BgfxGuiService::ScissorRect> BgfxGuiService::CurrentScissor() const {
|
|
if (scissorStack_.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
return scissorStack_.back();
|
|
}
|
|
|
|
std::optional<BgfxGuiService::ScissorRect> BgfxGuiService::BuildScissor(
|
|
const std::optional<ScissorRect>& scoped) const {
|
|
auto current = CurrentScissor();
|
|
if (current && scoped) {
|
|
return IntersectScissor(*current, *scoped);
|
|
}
|
|
if (current) {
|
|
return current;
|
|
}
|
|
return scoped;
|
|
}
|
|
|
|
void BgfxGuiService::SetScissor(const std::optional<ScissorRect>& scissor) const {
|
|
if (!scissor || scissor->width <= 0.0f || scissor->height <= 0.0f) {
|
|
bgfx::setScissor(0, 0, 0, 0);
|
|
return;
|
|
}
|
|
|
|
uint16_t x = static_cast<uint16_t>(std::clamp(scissor->x, 0.0f, static_cast<float>(frameWidth_)));
|
|
uint16_t y = static_cast<uint16_t>(std::clamp(scissor->y, 0.0f, static_cast<float>(frameHeight_)));
|
|
uint16_t w = static_cast<uint16_t>(std::clamp(scissor->width, 0.0f, static_cast<float>(frameWidth_ - x)));
|
|
uint16_t h = static_cast<uint16_t>(std::clamp(scissor->height, 0.0f, static_cast<float>(frameHeight_ - y)));
|
|
bgfx::setScissor(x, y, w, h);
|
|
}
|
|
|
|
void BgfxGuiService::SubmitRect(const GuiCommand& command, const std::optional<ScissorRect>& scissor) {
|
|
GuiColor color = command.color;
|
|
color.r = Clamp01(color.r);
|
|
color.g = Clamp01(color.g);
|
|
color.b = Clamp01(color.b);
|
|
color.a = Clamp01(color.a);
|
|
if (color.a > 0.0f) {
|
|
GuiVertex v0{command.rect.x, command.rect.y, 0.0f,
|
|
Clamp01(color.r), Clamp01(color.g), Clamp01(color.b), Clamp01(color.a), 0.0f, 0.0f};
|
|
GuiVertex v1{command.rect.x + command.rect.width, command.rect.y, 0.0f,
|
|
Clamp01(color.r), Clamp01(color.g), Clamp01(color.b), Clamp01(color.a), 1.0f, 0.0f};
|
|
GuiVertex v2{command.rect.x + command.rect.width, command.rect.y + command.rect.height, 0.0f,
|
|
Clamp01(color.r), Clamp01(color.g), Clamp01(color.b), Clamp01(color.a), 1.0f, 1.0f};
|
|
GuiVertex v3{command.rect.x, command.rect.y + command.rect.height, 0.0f,
|
|
Clamp01(color.r), Clamp01(color.g), Clamp01(color.b), Clamp01(color.a), 0.0f, 1.0f};
|
|
SubmitQuad(v0, v1, v2, v3, whiteTexture_, scissor);
|
|
}
|
|
|
|
if (command.borderWidth <= 0.0f) {
|
|
return;
|
|
}
|
|
|
|
GuiColor border = command.borderColor;
|
|
if (border.a <= 0.0f) {
|
|
return;
|
|
}
|
|
border.r = Clamp01(border.r);
|
|
border.g = Clamp01(border.g);
|
|
border.b = Clamp01(border.b);
|
|
border.a = Clamp01(border.a);
|
|
|
|
float bw = std::min(command.borderWidth, std::min(command.rect.width, command.rect.height));
|
|
float x = command.rect.x;
|
|
float y = command.rect.y;
|
|
float w = command.rect.width;
|
|
float h = command.rect.height;
|
|
|
|
GuiVertex vt0{x, y, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 0.0f};
|
|
GuiVertex vt1{x + w, y, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 0.0f};
|
|
GuiVertex vt2{x + w, y + bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 1.0f};
|
|
GuiVertex vt3{x, y + bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 1.0f};
|
|
SubmitQuad(vt0, vt1, vt2, vt3, whiteTexture_, scissor);
|
|
|
|
GuiVertex vb0{x, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 0.0f};
|
|
GuiVertex vb1{x + w, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 0.0f};
|
|
GuiVertex vb2{x + w, y + h, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 1.0f};
|
|
GuiVertex vb3{x, y + h, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 1.0f};
|
|
SubmitQuad(vb0, vb1, vb2, vb3, whiteTexture_, scissor);
|
|
|
|
GuiVertex vl0{x, y + bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 0.0f};
|
|
GuiVertex vl1{x + bw, y + bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 0.0f};
|
|
GuiVertex vl2{x + bw, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 1.0f};
|
|
GuiVertex vl3{x, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 1.0f};
|
|
SubmitQuad(vl0, vl1, vl2, vl3, whiteTexture_, scissor);
|
|
|
|
GuiVertex vr0{x + w - bw, y + bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 0.0f};
|
|
GuiVertex vr1{x + w, y + bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 0.0f};
|
|
GuiVertex vr2{x + w, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 1.0f, 1.0f};
|
|
GuiVertex vr3{x + w - bw, y + h - bw, 0.0f, border.r, border.g, border.b, border.a, 0.0f, 1.0f};
|
|
SubmitQuad(vr0, vr1, vr2, vr3, whiteTexture_, scissor);
|
|
}
|
|
|
|
void BgfxGuiService::SubmitText(const GuiCommand& command, const std::optional<ScissorRect>& scissor) {
|
|
if (command.text.empty()) {
|
|
return;
|
|
}
|
|
|
|
int fontSize = command.fontSize > 0.0f
|
|
? static_cast<int>(std::lround(command.fontSize))
|
|
: defaultFontSize_;
|
|
fontSize = std::max(8, fontSize);
|
|
|
|
const TextTexture* texture = GetTextTexture(command.text, fontSize);
|
|
if (!texture || !bgfx::isValid(texture->texture) || texture->width == 0 || texture->height == 0) {
|
|
return;
|
|
}
|
|
|
|
std::string alignX = ToLower(command.alignX);
|
|
std::string alignY = ToLower(command.alignY);
|
|
|
|
float x = command.rect.x;
|
|
float y = command.rect.y;
|
|
float width = static_cast<float>(texture->width);
|
|
float height = static_cast<float>(texture->height);
|
|
|
|
if (alignX == "center") {
|
|
x -= width * 0.5f;
|
|
} else if (alignX == "right") {
|
|
x -= width;
|
|
}
|
|
|
|
if (alignY == "center") {
|
|
y -= height * 0.5f;
|
|
} else if (alignY == "bottom") {
|
|
y -= height;
|
|
}
|
|
|
|
GuiColor color = command.color;
|
|
GuiVertex v0{x, y, 0.0f, color.r, color.g, color.b, color.a, 0.0f, 0.0f};
|
|
GuiVertex v1{x + width, y, 0.0f, color.r, color.g, color.b, color.a, 1.0f, 0.0f};
|
|
GuiVertex v2{x + width, y + height, 0.0f, color.r, color.g, color.b, color.a, 1.0f, 1.0f};
|
|
GuiVertex v3{x, y + height, 0.0f, color.r, color.g, color.b, color.a, 0.0f, 1.0f};
|
|
SubmitQuad(v0, v1, v2, v3, texture->texture, scissor);
|
|
}
|
|
|
|
void BgfxGuiService::SubmitSvg(const GuiCommand& command, const std::optional<ScissorRect>& scissor) {
|
|
if (command.svgPath.empty()) {
|
|
return;
|
|
}
|
|
|
|
int width = static_cast<int>(std::lround(command.rect.width));
|
|
int height = static_cast<int>(std::lround(command.rect.height));
|
|
if (width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
|
|
const SvgTexture* texture = GetSvgTexture(command.svgPath, width, height);
|
|
if (!texture || !bgfx::isValid(texture->texture)) {
|
|
return;
|
|
}
|
|
|
|
GuiColor color = command.svgTint;
|
|
color.r = Clamp01(color.r);
|
|
color.g = Clamp01(color.g);
|
|
color.b = Clamp01(color.b);
|
|
color.a = Clamp01(color.a);
|
|
GuiVertex v0{command.rect.x, command.rect.y, 0.0f, color.r, color.g, color.b, color.a, 0.0f, 0.0f};
|
|
GuiVertex v1{command.rect.x + command.rect.width, command.rect.y, 0.0f, color.r, color.g, color.b, color.a, 1.0f, 0.0f};
|
|
GuiVertex v2{command.rect.x + command.rect.width, command.rect.y + command.rect.height, 0.0f, color.r, color.g, color.b, color.a, 1.0f, 1.0f};
|
|
GuiVertex v3{command.rect.x, command.rect.y + command.rect.height, 0.0f, color.r, color.g, color.b, color.a, 0.0f, 1.0f};
|
|
SubmitQuad(v0, v1, v2, v3, texture->texture, scissor);
|
|
}
|
|
|
|
void BgfxGuiService::SubmitQuad(const GuiVertex& v0,
|
|
const GuiVertex& v1,
|
|
const GuiVertex& v2,
|
|
const GuiVertex& v3,
|
|
bgfx::TextureHandle texture,
|
|
const std::optional<ScissorRect>& scissor) {
|
|
if (!bgfx::isValid(program_) || !bgfx::isValid(texture)) {
|
|
return;
|
|
}
|
|
if (scissor && (scissor->width <= 0.0f || scissor->height <= 0.0f)) {
|
|
return;
|
|
}
|
|
|
|
if (bgfx::getAvailTransientVertexBuffer(4, layout_) < 4 ||
|
|
bgfx::getAvailTransientIndexBuffer(6) < 6) {
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "SubmitQuad", "Transient buffer exhausted");
|
|
}
|
|
return;
|
|
}
|
|
|
|
bgfx::TransientVertexBuffer tvb{};
|
|
bgfx::TransientIndexBuffer tib{};
|
|
bgfx::allocTransientVertexBuffer(&tvb, 4, layout_);
|
|
bgfx::allocTransientIndexBuffer(&tib, 6);
|
|
|
|
auto* vertices = reinterpret_cast<GuiVertex*>(tvb.data);
|
|
vertices[0] = v0;
|
|
vertices[1] = v1;
|
|
vertices[2] = v2;
|
|
vertices[3] = v3;
|
|
|
|
auto* indices = reinterpret_cast<uint16_t*>(tib.data);
|
|
indices[0] = 0;
|
|
indices[1] = 1;
|
|
indices[2] = 2;
|
|
indices[3] = 0;
|
|
indices[4] = 2;
|
|
indices[5] = 3;
|
|
|
|
float identity[16];
|
|
bx::mtxIdentity(identity);
|
|
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "SubmitQuad",
|
|
"vertex[0]: pos=[" + std::to_string(v0.x) + "," + std::to_string(v0.y) + "," + std::to_string(v0.z) +
|
|
"], color=[" + std::to_string(v0.r) + "," + std::to_string(v0.g) + "," + std::to_string(v0.b) + "," + std::to_string(v0.a) +
|
|
"], uv=[" + std::to_string(v0.u) + "," + std::to_string(v0.v) + "]");
|
|
logger_->Trace("BgfxGuiService", "SubmitQuad",
|
|
"uniforms: mvp=" + std::to_string(bgfx::isValid(modelViewProjUniform_)) +
|
|
", sampler=" + std::to_string(bgfx::isValid(sampler_)) +
|
|
", program=" + std::to_string(bgfx::isValid(program_)) +
|
|
", texture=" + std::to_string(bgfx::isValid(texture)) +
|
|
", viewId=" + std::to_string(viewId_));
|
|
logger_->Trace("BgfxGuiService", "SubmitQuad",
|
|
"viewProjection[0-3]=[" + std::to_string(viewProjection_[0]) + "," +
|
|
std::to_string(viewProjection_[1]) + "," +
|
|
std::to_string(viewProjection_[2]) + "," +
|
|
std::to_string(viewProjection_[3]) + "]");
|
|
}
|
|
|
|
SetScissor(scissor);
|
|
bgfx::setTransform(identity);
|
|
if (bgfx::isValid(modelViewProjUniform_)) {
|
|
bgfx::setUniform(modelViewProjUniform_, viewProjection_.data());
|
|
} else if (logger_) {
|
|
logger_->Error("BgfxGuiService::SubmitQuad: modelViewProjUniform_ is invalid!");
|
|
}
|
|
bgfx::setTexture(0, sampler_, texture);
|
|
bgfx::setVertexBuffer(0, &tvb, 0, 4);
|
|
bgfx::setIndexBuffer(&tib, 0, 6);
|
|
bgfx::setState(BGFX_STATE_WRITE_RGB |
|
|
BGFX_STATE_WRITE_A |
|
|
BGFX_STATE_BLEND_ALPHA |
|
|
BGFX_STATE_MSAA);
|
|
bgfx::submit(viewId_, program_);
|
|
}
|
|
|
|
const BgfxGuiService::TextTexture* BgfxGuiService::GetTextTexture(const std::string& text, int fontSize) {
|
|
if (text.empty()) {
|
|
return nullptr;
|
|
}
|
|
|
|
EnsureFontReady();
|
|
if (!freeType_ || !freeType_->ready || !freeType_->face) {
|
|
return nullptr;
|
|
}
|
|
|
|
TextKey key{text, fontSize};
|
|
auto it = textCache_.find(key);
|
|
if (it != textCache_.end()) {
|
|
it->second.lastUsedFrame = frameIndex_;
|
|
return &it->second;
|
|
}
|
|
|
|
FT_Face face = freeType_->face;
|
|
if (FT_Set_Pixel_Sizes(face, 0, fontSize) != 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
int ascent = face->size->metrics.ascender >> 6;
|
|
int descent = face->size->metrics.descender >> 6;
|
|
int height = ascent - descent;
|
|
int width = 0;
|
|
|
|
for (unsigned char ch : text) {
|
|
if (FT_Load_Char(face, ch, FT_LOAD_RENDER) != 0) {
|
|
continue;
|
|
}
|
|
width += face->glyph->advance.x >> 6;
|
|
}
|
|
|
|
if (width <= 0 || height <= 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<uint8_t> pixels(static_cast<size_t>(width * height * 4), 0);
|
|
int penX = 0;
|
|
for (unsigned char ch : text) {
|
|
if (FT_Load_Char(face, ch, FT_LOAD_RENDER) != 0) {
|
|
continue;
|
|
}
|
|
|
|
FT_GlyphSlot glyph = face->glyph;
|
|
FT_Bitmap& bitmap = glyph->bitmap;
|
|
int pitch = bitmap.pitch;
|
|
if (pitch < 0) {
|
|
pitch = -pitch;
|
|
}
|
|
|
|
int x0 = penX + glyph->bitmap_left;
|
|
int y0 = ascent - glyph->bitmap_top;
|
|
|
|
for (int row = 0; row < static_cast<int>(bitmap.rows); ++row) {
|
|
int y = y0 + row;
|
|
if (y < 0 || y >= height) {
|
|
continue;
|
|
}
|
|
for (int col = 0; col < static_cast<int>(bitmap.width); ++col) {
|
|
int x = x0 + col;
|
|
if (x < 0 || x >= width) {
|
|
continue;
|
|
}
|
|
uint8_t alpha = bitmap.buffer[row * pitch + col];
|
|
size_t idx = static_cast<size_t>((y * width + x) * 4);
|
|
pixels[idx + 0] = 255;
|
|
pixels[idx + 1] = 255;
|
|
pixels[idx + 2] = 255;
|
|
pixels[idx + 3] = alpha;
|
|
}
|
|
}
|
|
|
|
penX += glyph->advance.x >> 6;
|
|
}
|
|
|
|
TextTexture entry{};
|
|
entry.texture = CreateTexture(pixels.data(),
|
|
static_cast<uint32_t>(width),
|
|
static_cast<uint32_t>(height),
|
|
kGuiSamplerFlags);
|
|
entry.width = width;
|
|
entry.height = height;
|
|
entry.baseline = ascent;
|
|
entry.fontSize = fontSize;
|
|
entry.lastUsedFrame = frameIndex_;
|
|
|
|
auto [insertIt, inserted] = textCache_.emplace(std::move(key), entry);
|
|
if (!inserted) {
|
|
return nullptr;
|
|
}
|
|
return &insertIt->second;
|
|
}
|
|
|
|
const BgfxGuiService::SvgTexture* BgfxGuiService::GetSvgTexture(const std::string& path,
|
|
int width,
|
|
int height) {
|
|
if (path.empty()) {
|
|
return nullptr;
|
|
}
|
|
|
|
SvgKey key{ResolvePath(path).string(), width, height};
|
|
auto it = svgCache_.find(key);
|
|
if (it != svgCache_.end()) {
|
|
it->second.lastUsedFrame = frameIndex_;
|
|
return &it->second;
|
|
}
|
|
|
|
auto document = lunasvg::Document::loadFromFile(key.path);
|
|
if (!document) {
|
|
if (logger_) {
|
|
logger_->Warn("BgfxGuiService::GetSvgTexture: Failed to load " + key.path);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
auto bitmap = document->renderToBitmap(width, height);
|
|
if (!bitmap.valid()) {
|
|
return nullptr;
|
|
}
|
|
|
|
const uint8_t* data = bitmap.data();
|
|
const uint32_t w = static_cast<uint32_t>(bitmap.width());
|
|
const uint32_t h = static_cast<uint32_t>(bitmap.height());
|
|
if (!data || w == 0 || h == 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<uint8_t> rgba(static_cast<size_t>(w * h * 4), 0);
|
|
for (uint32_t i = 0; i < w * h; ++i) {
|
|
const uint8_t b = data[i * 4 + 0];
|
|
const uint8_t g = data[i * 4 + 1];
|
|
const uint8_t r = data[i * 4 + 2];
|
|
const uint8_t a = data[i * 4 + 3];
|
|
if (a > 0) {
|
|
rgba[i * 4 + 0] = static_cast<uint8_t>(std::min(255, (static_cast<int>(r) * 255) / a));
|
|
rgba[i * 4 + 1] = static_cast<uint8_t>(std::min(255, (static_cast<int>(g) * 255) / a));
|
|
rgba[i * 4 + 2] = static_cast<uint8_t>(std::min(255, (static_cast<int>(b) * 255) / a));
|
|
rgba[i * 4 + 3] = a;
|
|
} else {
|
|
rgba[i * 4 + 0] = 0;
|
|
rgba[i * 4 + 1] = 0;
|
|
rgba[i * 4 + 2] = 0;
|
|
rgba[i * 4 + 3] = 0;
|
|
}
|
|
}
|
|
|
|
SvgTexture entry{};
|
|
entry.texture = CreateTexture(rgba.data(), w, h, kGuiSamplerFlags);
|
|
entry.width = static_cast<int>(w);
|
|
entry.height = static_cast<int>(h);
|
|
entry.lastUsedFrame = frameIndex_;
|
|
|
|
auto [insertIt, inserted] = svgCache_.emplace(std::move(key), entry);
|
|
if (!inserted) {
|
|
return nullptr;
|
|
}
|
|
return &insertIt->second;
|
|
}
|
|
|
|
bgfx::TextureHandle BgfxGuiService::CreateTexture(const uint8_t* rgba,
|
|
uint32_t width,
|
|
uint32_t height,
|
|
uint64_t flags) const {
|
|
if (!rgba || width == 0 || height == 0) {
|
|
return BGFX_INVALID_HANDLE;
|
|
}
|
|
const uint32_t size = width * height * 4;
|
|
const bgfx::Memory* mem = bgfx::copy(rgba, size);
|
|
return bgfx::createTexture2D(static_cast<uint16_t>(width),
|
|
static_cast<uint16_t>(height),
|
|
false,
|
|
1,
|
|
bgfx::TextureFormat::RGBA8,
|
|
flags,
|
|
mem);
|
|
}
|
|
|
|
bgfx::ProgramHandle BgfxGuiService::CreateProgram(const char* vertexSource,
|
|
const char* fragmentSource) const {
|
|
if (!vertexSource || !fragmentSource) {
|
|
if (logger_) {
|
|
logger_->Error("BgfxGuiService::CreateProgram: null shader source");
|
|
}
|
|
return BGFX_INVALID_HANDLE;
|
|
}
|
|
|
|
bgfx::ShaderHandle vs = CreateShader("gui_vertex", vertexSource, true);
|
|
bgfx::ShaderHandle fs = CreateShader("gui_fragment", fragmentSource, false);
|
|
if (!bgfx::isValid(vs) || !bgfx::isValid(fs)) {
|
|
if (logger_) {
|
|
logger_->Error("BgfxGuiService::CreateProgram: shader compilation failed (vs=" +
|
|
std::to_string(bgfx::isValid(vs)) + ", fs=" +
|
|
std::to_string(bgfx::isValid(fs)) + ")");
|
|
}
|
|
if (bgfx::isValid(vs)) {
|
|
bgfx::destroy(vs);
|
|
}
|
|
if (bgfx::isValid(fs)) {
|
|
bgfx::destroy(fs);
|
|
}
|
|
return BGFX_INVALID_HANDLE;
|
|
}
|
|
|
|
bgfx::ProgramHandle program = bgfx::createProgram(vs, fs, true);
|
|
if (!bgfx::isValid(program) && logger_) {
|
|
logger_->Error("BgfxGuiService::CreateProgram: bgfx::createProgram failed to link shaders");
|
|
logger_->Trace("BgfxGuiService", "CreateProgram",
|
|
"renderer=" + std::string(RendererTypeName(bgfx::getRendererType())));
|
|
} else if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "CreateProgram", "GUI program created successfully");
|
|
}
|
|
return program;
|
|
}
|
|
|
|
bgfx::ShaderHandle BgfxGuiService::CreateShader(const std::string& label,
|
|
const std::string& source,
|
|
bool isVertex) const {
|
|
const bgfx::RendererType::Enum rendererType = bgfx::getRendererType();
|
|
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "CreateShader",
|
|
"label=" + label +
|
|
", renderer=" + std::string(RendererTypeName(rendererType)) +
|
|
", sourceLength=" + std::to_string(source.size()));
|
|
}
|
|
|
|
const bool isOpenGL = (rendererType == bgfx::RendererType::OpenGL ||
|
|
rendererType == bgfx::RendererType::OpenGLES);
|
|
|
|
if (isOpenGL) {
|
|
// For OpenGL: Just copy GLSL source directly
|
|
const uint32_t sourceSize = static_cast<uint32_t>(source.size());
|
|
const bgfx::Memory* mem = bgfx::copy(source.c_str(), sourceSize + 1);
|
|
|
|
bgfx::ShaderHandle handle = bgfx::createShader(mem);
|
|
if (!bgfx::isValid(handle) && logger_) {
|
|
logger_->Error("BgfxGuiService: Failed to create shader handle for " + label);
|
|
}
|
|
return handle;
|
|
}
|
|
|
|
// For Vulkan/Metal/DX: Compile to SPIRV and wrap in bgfx binary format
|
|
shaderc::Compiler compiler;
|
|
shaderc::CompileOptions options;
|
|
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_1);
|
|
options.SetAutoBindUniforms(true);
|
|
// Do NOT use SetAutoMapLocations - it overrides explicit layout(location=N) declarations
|
|
// and assigns locations alphabetically by variable name, breaking the vertex layout.
|
|
// GUI shaders already specify explicit locations matching the VertexLayout.
|
|
// options.SetAutoMapLocations(true);
|
|
|
|
shaderc_shader_kind kind = isVertex ? shaderc_vertex_shader : shaderc_fragment_shader;
|
|
auto result = compiler.CompileGlslToSpv(source, kind, label.c_str(), options);
|
|
if (result.GetCompilationStatus() != shaderc_compilation_status_success) {
|
|
if (logger_) {
|
|
logger_->Error("BgfxGuiService::CreateShader: " + label + "\n" + result.GetErrorMessage());
|
|
}
|
|
return BGFX_INVALID_HANDLE;
|
|
}
|
|
|
|
std::vector<uint32_t> spirv(result.cbegin(), result.cend());
|
|
|
|
std::vector<GuiShaderUniform> uniforms;
|
|
std::vector<bgfx::Attrib::Enum> attributes;
|
|
if (isVertex) {
|
|
uniforms.push_back(GuiShaderUniform{
|
|
"u_modelViewProj",
|
|
bgfx::UniformType::Mat4,
|
|
1,
|
|
0,
|
|
4,
|
|
0,
|
|
0,
|
|
0
|
|
});
|
|
attributes = {bgfx::Attrib::Position, bgfx::Attrib::Color0, bgfx::Attrib::TexCoord0};
|
|
} else {
|
|
uniforms.push_back(GuiShaderUniform{
|
|
"s_tex",
|
|
bgfx::UniformType::Sampler,
|
|
1,
|
|
0,
|
|
1,
|
|
0,
|
|
0,
|
|
0
|
|
});
|
|
}
|
|
|
|
if (logger_) {
|
|
logger_->Trace("BgfxGuiService", "CreateShader",
|
|
"uniforms=" + std::to_string(uniforms.size()) +
|
|
", attributes=" + std::to_string(attributes.size()));
|
|
}
|
|
|
|
// Wrap SPIRV with bgfx binary format.
|
|
constexpr uint8_t kBgfxShaderVersion = 11;
|
|
constexpr uint32_t kMagicVSH = ('V') | ('S' << 8) | ('H' << 16) | (kBgfxShaderVersion << 24);
|
|
constexpr uint32_t kMagicFSH = ('F') | ('S' << 8) | ('H' << 16) | (kBgfxShaderVersion << 24);
|
|
const uint32_t magic = isVertex ? kMagicVSH : kMagicFSH;
|
|
const uint32_t varyingHash = 0x47554901; // "GUI" + 0x01
|
|
const uint32_t inputHash = varyingHash;
|
|
const uint32_t outputHash = varyingHash;
|
|
const uint32_t spirvSize = static_cast<uint32_t>(spirv.size() * sizeof(uint32_t));
|
|
const uint32_t uniformDataSize = 2 +
|
|
static_cast<uint32_t>(uniforms.size()) * (1 + 0 + 1 + 1 + 2 + 2 + 1 + 1 + 2) +
|
|
static_cast<uint32_t>(std::accumulate(
|
|
uniforms.begin(),
|
|
uniforms.end(),
|
|
size_t{0},
|
|
[](size_t total, const auto& un) { return total + un.name.size(); }));
|
|
const uint32_t attribDataSize = 1 + static_cast<uint32_t>(attributes.size()) * 2;
|
|
const uint32_t totalSize = 4 + 4 + 4 + uniformDataSize + 4 + spirvSize + 1 + attribDataSize + 2;
|
|
|
|
const bgfx::Memory* mem = bgfx::alloc(totalSize);
|
|
uint8_t* data = mem->data;
|
|
uint32_t offset = 0;
|
|
|
|
std::memcpy(data + offset, &magic, 4); offset += 4;
|
|
std::memcpy(data + offset, &inputHash, 4); offset += 4;
|
|
std::memcpy(data + offset, &outputHash, 4); offset += 4;
|
|
const uint16_t uniformSize = WriteUniformArray(data, offset, uniforms, !isVertex);
|
|
std::memcpy(data + offset, &spirvSize, 4); offset += 4;
|
|
std::memcpy(data + offset, spirv.data(), spirvSize); offset += spirvSize;
|
|
data[offset] = 0;
|
|
offset += 1;
|
|
|
|
const uint8_t numAttr = static_cast<uint8_t>(attributes.size());
|
|
std::memcpy(data + offset, &numAttr, sizeof(numAttr));
|
|
offset += sizeof(numAttr);
|
|
for (auto attr : attributes) {
|
|
const uint16_t attrId = GuiAttribToId(attr);
|
|
std::memcpy(data + offset, &attrId, sizeof(attrId));
|
|
offset += sizeof(attrId);
|
|
}
|
|
std::memcpy(data + offset, &uniformSize, sizeof(uniformSize));
|
|
|
|
bgfx::ShaderHandle handle = bgfx::createShader(mem);
|
|
if (!bgfx::isValid(handle) && logger_) {
|
|
logger_->Error("BgfxGuiService: Failed to create shader handle for " + label);
|
|
}
|
|
return handle;
|
|
}
|
|
|
|
void BgfxGuiService::PruneTextCache() {
|
|
if (textCache_.size() <= maxTextCacheEntries_) {
|
|
return;
|
|
}
|
|
while (textCache_.size() > maxTextCacheEntries_) {
|
|
auto oldest = std::min_element(textCache_.begin(), textCache_.end(),
|
|
[](const auto& left, const auto& right) {
|
|
return left.second.lastUsedFrame < right.second.lastUsedFrame;
|
|
});
|
|
if (oldest == textCache_.end()) {
|
|
break;
|
|
}
|
|
if (bgfx::isValid(oldest->second.texture)) {
|
|
bgfx::destroy(oldest->second.texture);
|
|
}
|
|
textCache_.erase(oldest);
|
|
}
|
|
}
|
|
|
|
void BgfxGuiService::PruneSvgCache() {
|
|
if (svgCache_.size() <= maxSvgCacheEntries_) {
|
|
return;
|
|
}
|
|
while (svgCache_.size() > maxSvgCacheEntries_) {
|
|
auto oldest = std::min_element(svgCache_.begin(), svgCache_.end(),
|
|
[](const auto& left, const auto& right) {
|
|
return left.second.lastUsedFrame < right.second.lastUsedFrame;
|
|
});
|
|
if (oldest == svgCache_.end()) {
|
|
break;
|
|
}
|
|
if (bgfx::isValid(oldest->second.texture)) {
|
|
bgfx::destroy(oldest->second.texture);
|
|
}
|
|
svgCache_.erase(oldest);
|
|
}
|
|
}
|
|
|
|
} // namespace sdl3cpp::services::impl
|