mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
1060 lines
40 KiB
C++
1060 lines
40 KiB
C++
#include "gui_renderer.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <shaderc/shaderc.hpp>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include "font8x8_basic.h"
|
|
|
|
namespace sdl3cpp::services::impl {
|
|
namespace {
|
|
|
|
bool ExtractAttribute(const std::string& source, const char* name, std::string& outValue) {
|
|
std::string key = name;
|
|
size_t pos = source.find(key);
|
|
while (pos != std::string::npos) {
|
|
size_t eq = source.find('=', pos + key.size());
|
|
if (eq == std::string::npos) {
|
|
break;
|
|
}
|
|
size_t valueStart = eq + 1;
|
|
while (valueStart < source.size() &&
|
|
std::isspace(static_cast<unsigned char>(source[valueStart]))) {
|
|
valueStart++;
|
|
}
|
|
if (valueStart >= source.size()) {
|
|
break;
|
|
}
|
|
char quote = source[valueStart];
|
|
if (quote != '\"' && quote != '\'') {
|
|
break;
|
|
}
|
|
size_t valueEnd = source.find(quote, valueStart + 1);
|
|
if (valueEnd == std::string::npos) {
|
|
break;
|
|
}
|
|
outValue = source.substr(valueStart + 1, valueEnd - valueStart - 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
float ParseFloatValue(const std::string& text) {
|
|
try {
|
|
size_t idx = 0;
|
|
return std::stof(text, &idx);
|
|
} catch (...) {
|
|
return 0.0f;
|
|
}
|
|
}
|
|
|
|
GuiColor ParseColorString(const std::string& text, const GuiColor& fallback) {
|
|
if (text.empty() || text[0] != '#') {
|
|
return fallback;
|
|
}
|
|
try {
|
|
if (text.size() == 7) {
|
|
unsigned int rgb = std::stoul(text.substr(1), nullptr, 16);
|
|
return {((rgb >> 16) & 0xFF) / 255.0f, ((rgb >> 8) & 0xFF) / 255.0f,
|
|
(rgb & 0xFF) / 255.0f, 1.0f};
|
|
}
|
|
if (text.size() == 9) {
|
|
unsigned int rgba = std::stoul(text.substr(1), nullptr, 16);
|
|
return {((rgba >> 24) & 0xFF) / 255.0f, ((rgba >> 16) & 0xFF) / 255.0f,
|
|
((rgba >> 8) & 0xFF) / 255.0f, (rgba & 0xFF) / 255.0f};
|
|
}
|
|
} catch (...) {
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
ParsedSvg ParseSvgFile(const std::filesystem::path& path) {
|
|
std::ifstream file(path);
|
|
if (!file) {
|
|
throw std::runtime_error("Failed to open SVG file: " + path.string());
|
|
}
|
|
std::string data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
ParsedSvg result;
|
|
std::string value;
|
|
if (ExtractAttribute(data, "viewBox", value)) {
|
|
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
|
std::sscanf(value.c_str(), "%f %f %f %f", &x, &y, &w, &h);
|
|
if (w > 0.0f && h > 0.0f) {
|
|
result.viewWidth = w;
|
|
result.viewHeight = h;
|
|
}
|
|
}
|
|
if (ExtractAttribute(data, "width", value)) {
|
|
result.viewWidth = ParseFloatValue(value);
|
|
}
|
|
if (ExtractAttribute(data, "height", value)) {
|
|
result.viewHeight = ParseFloatValue(value);
|
|
}
|
|
if (result.viewWidth <= 0.0f) {
|
|
result.viewWidth = 128.0f;
|
|
}
|
|
if (result.viewHeight <= 0.0f) {
|
|
result.viewHeight = 128.0f;
|
|
}
|
|
|
|
size_t search = 0;
|
|
while (true) {
|
|
size_t tagStart = data.find("<circle", search);
|
|
if (tagStart == std::string::npos) {
|
|
break;
|
|
}
|
|
size_t tagEnd = data.find('>', tagStart);
|
|
if (tagEnd == std::string::npos) {
|
|
break;
|
|
}
|
|
std::string tag = data.substr(tagStart, tagEnd - tagStart);
|
|
SvgCircle circle;
|
|
std::string attr;
|
|
if (ExtractAttribute(tag, "cx", attr)) {
|
|
circle.cx = ParseFloatValue(attr);
|
|
}
|
|
if (ExtractAttribute(tag, "cy", attr)) {
|
|
circle.cy = ParseFloatValue(attr);
|
|
}
|
|
if (ExtractAttribute(tag, "r", attr)) {
|
|
circle.r = ParseFloatValue(attr);
|
|
}
|
|
if (ExtractAttribute(tag, "fill", attr)) {
|
|
circle.color = ParseColorString(attr, circle.color);
|
|
}
|
|
result.circles.push_back(circle);
|
|
search = tagEnd + 1;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
GuiCommand::RectData IntersectRect(const GuiCommand::RectData& a, const GuiCommand::RectData& b) {
|
|
GuiCommand::RectData result;
|
|
result.x = std::max(a.x, b.x);
|
|
result.y = std::max(a.y, b.y);
|
|
float right = std::min(a.x + a.width, b.x + b.width);
|
|
float bottom = std::min(a.y + a.height, b.y + b.height);
|
|
result.width = std::max(0.0f, right - result.x);
|
|
result.height = std::max(0.0f, bottom - result.y);
|
|
return result;
|
|
}
|
|
|
|
bool RectHasArea(const GuiCommand::RectData& rect) {
|
|
return rect.width > 0.0f && rect.height > 0.0f;
|
|
}
|
|
|
|
struct ClipPoint {
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
};
|
|
|
|
enum class ClipEdge {
|
|
Left,
|
|
Right,
|
|
Top,
|
|
Bottom
|
|
};
|
|
|
|
bool IsInsideClip(const ClipPoint& point, const GuiCommand::RectData& rect, ClipEdge edge) {
|
|
switch (edge) {
|
|
case ClipEdge::Left:
|
|
return point.x >= rect.x;
|
|
case ClipEdge::Right:
|
|
return point.x <= rect.x + rect.width;
|
|
case ClipEdge::Top:
|
|
return point.y >= rect.y;
|
|
case ClipEdge::Bottom:
|
|
return point.y <= rect.y + rect.height;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ClipPoint IntersectClipEdge(const ClipPoint& a, const ClipPoint& b,
|
|
const GuiCommand::RectData& rect, ClipEdge edge) {
|
|
ClipPoint result = a;
|
|
float dx = b.x - a.x;
|
|
float dy = b.y - a.y;
|
|
|
|
if (edge == ClipEdge::Left || edge == ClipEdge::Right) {
|
|
float clipX = (edge == ClipEdge::Left) ? rect.x : (rect.x + rect.width);
|
|
float t = (dx != 0.0f) ? (clipX - a.x) / dx : 0.0f;
|
|
result.x = clipX;
|
|
result.y = a.y + t * dy;
|
|
} else {
|
|
float clipY = (edge == ClipEdge::Top) ? rect.y : (rect.y + rect.height);
|
|
float t = (dy != 0.0f) ? (clipY - a.y) / dy : 0.0f;
|
|
result.x = a.x + t * dx;
|
|
result.y = clipY;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<ClipPoint> ClipPolygonToRect(const std::vector<ClipPoint>& polygon,
|
|
const GuiCommand::RectData& rect) {
|
|
std::vector<ClipPoint> output = polygon;
|
|
for (ClipEdge edge : {ClipEdge::Left, ClipEdge::Right, ClipEdge::Top, ClipEdge::Bottom}) {
|
|
if (output.empty()) {
|
|
break;
|
|
}
|
|
std::vector<ClipPoint> input = output;
|
|
output.clear();
|
|
|
|
for (size_t i = 0; i < input.size(); ++i) {
|
|
const ClipPoint& current = input[i];
|
|
const ClipPoint& previous = input[(i + input.size() - 1) % input.size()];
|
|
bool currentInside = IsInsideClip(current, rect, edge);
|
|
bool previousInside = IsInsideClip(previous, rect, edge);
|
|
|
|
if (currentInside) {
|
|
if (!previousInside) {
|
|
output.push_back(IntersectClipEdge(previous, current, rect, edge));
|
|
}
|
|
output.push_back(current);
|
|
} else if (previousInside) {
|
|
output.push_back(IntersectClipEdge(previous, current, rect, edge));
|
|
}
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
shaderc_shader_kind ShadercKindFromStage(VkShaderStageFlagBits stage) {
|
|
switch (stage) {
|
|
case VK_SHADER_STAGE_VERTEX_BIT:
|
|
return shaderc_vertex_shader;
|
|
case VK_SHADER_STAGE_FRAGMENT_BIT:
|
|
return shaderc_fragment_shader;
|
|
case VK_SHADER_STAGE_GEOMETRY_BIT:
|
|
return shaderc_geometry_shader;
|
|
case VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT:
|
|
return shaderc_tess_control_shader;
|
|
case VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT:
|
|
return shaderc_tess_evaluation_shader;
|
|
case VK_SHADER_STAGE_COMPUTE_BIT:
|
|
return shaderc_compute_shader;
|
|
default:
|
|
return shaderc_glsl_infer_from_source;
|
|
}
|
|
}
|
|
|
|
std::vector<uint8_t> ReadShaderFile(const std::filesystem::path& path,
|
|
VkShaderStageFlagBits stage,
|
|
ILogger* logger) {
|
|
if (logger) {
|
|
logger->Trace("GuiRenderer", "ReadShaderFile",
|
|
"path=" + path.string() + ", stage=" +
|
|
std::to_string(static_cast<int>(stage)));
|
|
}
|
|
|
|
if (path.empty()) {
|
|
throw std::runtime_error("Shader path is empty");
|
|
}
|
|
|
|
std::filesystem::path shaderPath = path;
|
|
if (shaderPath.extension() == ".spv") {
|
|
std::filesystem::path sourcePath = shaderPath;
|
|
sourcePath.replace_extension();
|
|
if (logger) {
|
|
logger->Trace("GuiRenderer", "ReadShaderFile",
|
|
"usingSource=" + sourcePath.string());
|
|
}
|
|
shaderPath = sourcePath;
|
|
}
|
|
|
|
if (!std::filesystem::exists(shaderPath)) {
|
|
throw std::runtime_error("Shader file not found: " + shaderPath.string() +
|
|
"\n\nPlease ensure the shader source (.vert/.frag/etc.) exists.");
|
|
}
|
|
|
|
if (!std::filesystem::is_regular_file(shaderPath)) {
|
|
throw std::runtime_error("Path is not a regular file: " + shaderPath.string());
|
|
}
|
|
|
|
std::ifstream sourceFile(shaderPath);
|
|
if (!sourceFile) {
|
|
throw std::runtime_error("Failed to open shader source: " + shaderPath.string());
|
|
}
|
|
std::string source((std::istreambuf_iterator<char>(sourceFile)),
|
|
std::istreambuf_iterator<char>());
|
|
sourceFile.close();
|
|
|
|
shaderc::Compiler compiler;
|
|
shaderc::CompileOptions options;
|
|
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2);
|
|
|
|
shaderc_shader_kind kind = ShadercKindFromStage(stage);
|
|
auto result = compiler.CompileGlslToSpv(source, kind, shaderPath.string().c_str(), options);
|
|
if (result.GetCompilationStatus() != shaderc_compilation_status_success) {
|
|
std::string error = result.GetErrorMessage();
|
|
if (logger) {
|
|
logger->Error("GuiRenderer shader compilation failed: " + shaderPath.string() +
|
|
"\n" + error);
|
|
}
|
|
throw std::runtime_error("Shader compilation failed: " + shaderPath.string() +
|
|
"\n" + error);
|
|
}
|
|
|
|
std::vector<uint32_t> spirv(result.cbegin(), result.cend());
|
|
std::vector<uint8_t> buffer(spirv.size() * sizeof(uint32_t));
|
|
if (!buffer.empty()) {
|
|
std::memcpy(buffer.data(), spirv.data(), buffer.size());
|
|
}
|
|
if (logger) {
|
|
logger->Trace("GuiRenderer", "ReadShaderFile",
|
|
"compiledBytes=" + std::to_string(buffer.size()));
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
std::vector<uint8_t> ReadShaderSource(const std::string& source,
|
|
VkShaderStageFlagBits stage,
|
|
const std::string& label,
|
|
ILogger* logger) {
|
|
if (logger) {
|
|
logger->Trace("GuiRenderer", "ReadShaderSource",
|
|
"label=" + label + ", stage=" +
|
|
std::to_string(static_cast<int>(stage)));
|
|
}
|
|
|
|
if (source.empty()) {
|
|
throw std::runtime_error("Shader source is empty");
|
|
}
|
|
|
|
shaderc::Compiler compiler;
|
|
shaderc::CompileOptions options;
|
|
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2);
|
|
|
|
shaderc_shader_kind kind = ShadercKindFromStage(stage);
|
|
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("GuiRenderer shader compilation failed: " + label + "\n" + error);
|
|
}
|
|
throw std::runtime_error("Shader compilation failed: " + label + "\n" + error);
|
|
}
|
|
|
|
std::vector<uint32_t> spirv(result.cbegin(), result.cend());
|
|
std::vector<uint8_t> buffer(spirv.size() * sizeof(uint32_t));
|
|
if (!buffer.empty()) {
|
|
std::memcpy(buffer.data(), spirv.data(), buffer.size());
|
|
}
|
|
if (logger) {
|
|
logger->Trace("GuiRenderer", "ReadShaderSource",
|
|
"compiledBytes=" + std::to_string(buffer.size()));
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
const char* kGuiVertexSource = R"(
|
|
#version 450
|
|
|
|
layout(location = 0) in vec3 inPos;
|
|
layout(location = 1) in vec4 inColor;
|
|
|
|
layout(location = 0) out vec4 fragColor;
|
|
|
|
layout(push_constant) uniform PushConstants {
|
|
mat4 model;
|
|
mat4 viewProj;
|
|
// Extended fields for PBR/atmospherics (ignored by basic shaders)
|
|
mat4 view;
|
|
mat4 proj;
|
|
mat4 lightViewProj;
|
|
vec3 cameraPos;
|
|
float time;
|
|
// Atmospherics parameters
|
|
float ambientStrength;
|
|
float fogDensity;
|
|
float fogStart;
|
|
float fogEnd;
|
|
vec3 fogColor;
|
|
float gamma;
|
|
float exposure;
|
|
int enableShadows;
|
|
int enableFog;
|
|
} pushConstants;
|
|
|
|
void main() {
|
|
fragColor = inColor;
|
|
vec4 worldPos = pushConstants.model * vec4(inPos, 1.0);
|
|
gl_Position = pushConstants.viewProj * worldPos;
|
|
}
|
|
)";
|
|
|
|
const char* kGuiFragmentSource = R"(
|
|
#version 450
|
|
|
|
layout(location = 0) in vec4 fragColor;
|
|
layout(location = 0) out vec4 outColor;
|
|
|
|
void main() {
|
|
outColor = fragColor;
|
|
}
|
|
)";
|
|
|
|
} // namespace
|
|
|
|
GuiRenderer::GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFormat swapchainFormat,
|
|
VkRenderPass renderPass, const std::filesystem::path& scriptDirectory,
|
|
std::shared_ptr<IBufferService> bufferService,
|
|
std::shared_ptr<ILogger> logger)
|
|
: device_(device),
|
|
physicalDevice_(physicalDevice),
|
|
swapchainFormat_(swapchainFormat),
|
|
renderPass_(renderPass),
|
|
scriptDirectory_(scriptDirectory),
|
|
bufferService_(std::move(bufferService)),
|
|
logger_(std::move(logger)) {
|
|
}
|
|
|
|
GuiRenderer::~GuiRenderer() {
|
|
CleanupBuffers();
|
|
CleanupPipeline();
|
|
}
|
|
|
|
bool GuiRenderer::IsReady() const {
|
|
return pipeline_ != VK_NULL_HANDLE && !vertices_.empty();
|
|
}
|
|
|
|
void GuiRenderer::Prepare(const std::vector<GuiCommand>& commands, uint32_t width, uint32_t height) {
|
|
if (width == 0 || height == 0) {
|
|
return;
|
|
}
|
|
|
|
viewportWidth_ = width;
|
|
viewportHeight_ = height;
|
|
|
|
// Create pipeline if needed
|
|
if (pipeline_ == VK_NULL_HANDLE) {
|
|
CreatePipeline(renderPass_, {width, height});
|
|
}
|
|
|
|
// Generate geometry from GUI commands
|
|
GenerateGuiGeometry(commands, width, height);
|
|
|
|
// Create/update vertex and index buffers
|
|
if (!vertices_.empty() && !indices_.empty()) {
|
|
CreateVertexAndIndexBuffers(vertices_.size(), indices_.size());
|
|
}
|
|
}
|
|
|
|
void GuiRenderer::RenderToSwapchain(VkCommandBuffer commandBuffer, VkRenderPass renderPass) {
|
|
if (!IsReady() || vertices_.empty() || indices_.empty()) {
|
|
return;
|
|
}
|
|
|
|
// Bind pipeline
|
|
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_);
|
|
|
|
// Bind vertex and index buffers
|
|
VkDeviceSize offset = 0;
|
|
vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertexBuffer_, &offset);
|
|
vkCmdBindIndexBuffer(commandBuffer, indexBuffer_, 0, VK_INDEX_TYPE_UINT32);
|
|
|
|
// Push identity matrices for 2D GUI rendering (no 3D transformation)
|
|
// The GUI coordinates are already in NDC space, so we use identity matrices
|
|
struct GuiPushConstants {
|
|
float model[16]; // Identity matrix
|
|
float viewProj[16]; // Identity matrix
|
|
} pushConstants{};
|
|
|
|
// Initialize as identity matrices
|
|
pushConstants.model[0] = 1.0f;
|
|
pushConstants.model[5] = 1.0f;
|
|
pushConstants.model[10] = 1.0f;
|
|
pushConstants.model[15] = 1.0f;
|
|
|
|
pushConstants.viewProj[0] = 1.0f;
|
|
pushConstants.viewProj[5] = 1.0f;
|
|
pushConstants.viewProj[10] = 1.0f;
|
|
pushConstants.viewProj[15] = 1.0f;
|
|
|
|
vkCmdPushConstants(commandBuffer, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
|
0, sizeof(GuiPushConstants), &pushConstants);
|
|
|
|
// Draw
|
|
vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(indices_.size()), 1, 0, 0, 0);
|
|
}
|
|
|
|
void GuiRenderer::Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass) {
|
|
if (width == viewportWidth_ && height == viewportHeight_ &&
|
|
format == swapchainFormat_ && renderPass == renderPass_) {
|
|
return;
|
|
}
|
|
UpdateFormat(format);
|
|
viewportWidth_ = width;
|
|
viewportHeight_ = height;
|
|
renderPass_ = renderPass;
|
|
|
|
// Recreate pipeline for new viewport size
|
|
if (pipeline_ != VK_NULL_HANDLE) {
|
|
CleanupPipeline();
|
|
CreatePipeline(renderPass_, {width, height});
|
|
}
|
|
}
|
|
|
|
void GuiRenderer::GenerateGuiGeometry(const std::vector<GuiCommand>& commands, uint32_t width, uint32_t height) {
|
|
vertices_.clear();
|
|
indices_.clear();
|
|
|
|
// Convert screen coordinates to NDC (-1 to 1)
|
|
auto toNDC = [width, height](float x, float y) -> std::pair<float, float> {
|
|
return {
|
|
(x / static_cast<float>(width)) * 2.0f - 1.0f,
|
|
(y / static_cast<float>(height)) * 2.0f - 1.0f
|
|
};
|
|
};
|
|
|
|
std::vector<GuiCommand::RectData> clipStack;
|
|
clipStack.push_back({0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)});
|
|
|
|
auto addQuad = [&](const GuiCommand::RectData& rect, const GuiColor& color,
|
|
const GuiCommand::RectData& clipRect) {
|
|
GuiCommand::RectData clipped = IntersectRect(rect, clipRect);
|
|
if (!RectHasArea(clipped)) {
|
|
return;
|
|
}
|
|
auto [x0, y0] = toNDC(clipped.x, clipped.y);
|
|
auto [x1, y1] = toNDC(clipped.x + clipped.width, clipped.y + clipped.height);
|
|
|
|
uint32_t baseIndex = static_cast<uint32_t>(vertices_.size());
|
|
vertices_.push_back({x0, y0, 0.0f, color.r, color.g, color.b, color.a});
|
|
vertices_.push_back({x1, y0, 0.0f, color.r, color.g, color.b, color.a});
|
|
vertices_.push_back({x1, y1, 0.0f, color.r, color.g, color.b, color.a});
|
|
vertices_.push_back({x0, y1, 0.0f, color.r, color.g, color.b, color.a});
|
|
|
|
indices_.push_back(baseIndex + 0);
|
|
indices_.push_back(baseIndex + 1);
|
|
indices_.push_back(baseIndex + 2);
|
|
indices_.push_back(baseIndex + 0);
|
|
indices_.push_back(baseIndex + 2);
|
|
indices_.push_back(baseIndex + 3);
|
|
};
|
|
|
|
auto addClippedPolygon = [&](const std::vector<ClipPoint>& polygon,
|
|
const GuiColor& color,
|
|
const GuiCommand::RectData& clipRect) {
|
|
std::vector<ClipPoint> clipped = ClipPolygonToRect(polygon, clipRect);
|
|
if (clipped.size() < 3) {
|
|
return;
|
|
}
|
|
uint32_t baseIndex = static_cast<uint32_t>(vertices_.size());
|
|
for (const auto& point : clipped) {
|
|
auto [x, y] = toNDC(point.x, point.y);
|
|
vertices_.push_back({x, y, 0.0f, color.r, color.g, color.b, color.a});
|
|
}
|
|
for (size_t i = 1; i + 1 < clipped.size(); ++i) {
|
|
indices_.push_back(baseIndex);
|
|
indices_.push_back(baseIndex + static_cast<uint32_t>(i));
|
|
indices_.push_back(baseIndex + static_cast<uint32_t>(i + 1));
|
|
}
|
|
};
|
|
|
|
for (const auto& cmd : commands) {
|
|
if (cmd.type == GuiCommand::Type::ClipPush) {
|
|
GuiCommand::RectData nextClip = IntersectRect(clipStack.back(), cmd.rect);
|
|
clipStack.push_back(nextClip);
|
|
continue;
|
|
}
|
|
if (cmd.type == GuiCommand::Type::ClipPop) {
|
|
if (clipStack.size() > 1) {
|
|
clipStack.pop_back();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const GuiCommand::RectData& activeClip = clipStack.back();
|
|
if (!RectHasArea(activeClip)) {
|
|
continue;
|
|
}
|
|
|
|
if (cmd.type == GuiCommand::Type::Rect) {
|
|
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) {
|
|
float innerHeight = std::max(0.0f, cmd.rect.height - border * 2.0f);
|
|
GuiCommand::RectData top{cmd.rect.x, cmd.rect.y, cmd.rect.width, border};
|
|
GuiCommand::RectData bottom{cmd.rect.x, cmd.rect.y + cmd.rect.height - border,
|
|
cmd.rect.width, border};
|
|
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);
|
|
}
|
|
}
|
|
} else if (cmd.type == GuiCommand::Type::Text) {
|
|
// Render text using 8x8 bitmap font
|
|
if (cmd.text.empty()) {
|
|
continue;
|
|
}
|
|
|
|
// Font metrics (8x8 bitmap font)
|
|
const float charWidth = cmd.fontSize * 0.6f; // Slightly wider for better appearance
|
|
const float charHeight = cmd.fontSize;
|
|
const float charSpacing = charWidth * 0.05f; // Tighter spacing
|
|
|
|
// Calculate text width for alignment
|
|
float textWidth = cmd.text.size() * (charWidth + charSpacing);
|
|
|
|
// Calculate text position based on alignment
|
|
// Text commands use bounds field, not rect field
|
|
GuiCommand::RectData textBounds = cmd.bounds;
|
|
if (!cmd.hasBounds) {
|
|
textBounds = {
|
|
cmd.rect.x,
|
|
cmd.rect.y,
|
|
cmd.fontSize * static_cast<float>(std::max<size_t>(1, cmd.text.size())),
|
|
cmd.fontSize
|
|
};
|
|
}
|
|
|
|
float startX = textBounds.x;
|
|
float startY = textBounds.y;
|
|
|
|
if (cmd.alignX == "center") {
|
|
startX += (textBounds.width - textWidth) * 0.5f;
|
|
} else if (cmd.alignX == "right") {
|
|
startX += textBounds.width - textWidth;
|
|
}
|
|
|
|
if (cmd.alignY == "center") {
|
|
startY += (textBounds.height - charHeight) * 0.5f;
|
|
} else if (cmd.alignY == "bottom") {
|
|
startY += textBounds.height - charHeight;
|
|
}
|
|
|
|
GuiCommand::RectData textClip = activeClip;
|
|
if (cmd.hasClipRect) {
|
|
textClip = IntersectRect(textClip, cmd.clipRect);
|
|
}
|
|
if (!RectHasArea(textClip)) {
|
|
continue;
|
|
}
|
|
|
|
// Render each character as a small quad
|
|
float x = startX;
|
|
for (char c : cmd.text) {
|
|
// Only render printable ASCII characters
|
|
if (c >= 32 && c < 127) {
|
|
// Get character bitmap from font8x8_basic
|
|
const uint8_t* glyph = font8x8_basic[static_cast<unsigned char>(c)];
|
|
|
|
// Render each pixel of the 8x8 glyph
|
|
for (int row = 0; row < 8; ++row) {
|
|
uint8_t rowData = glyph[row];
|
|
for (int col = 0; col < 8; ++col) {
|
|
if (rowData & (1 << col)) {
|
|
// This pixel is on, render a small quad
|
|
// Add slight overlap (1.15x) for smoother appearance
|
|
const float pixelScale = 1.15f;
|
|
float pixelWidth = (charWidth / 8.0f) * pixelScale;
|
|
float pixelHeight = (charHeight / 8.0f) * pixelScale;
|
|
float px = x + col * (charWidth / 8.0f) - (pixelWidth - charWidth / 8.0f) * 0.5f;
|
|
float py = startY + row * (charHeight / 8.0f) - (pixelHeight - charHeight / 8.0f) * 0.5f;
|
|
float pw = pixelWidth;
|
|
float ph = pixelHeight;
|
|
|
|
GuiCommand::RectData pixelRect{px, py, pw, ph};
|
|
addQuad(pixelRect, cmd.color, textClip);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
x += charWidth + charSpacing;
|
|
}
|
|
} else if (cmd.type == GuiCommand::Type::Svg) {
|
|
if (cmd.svgPath.empty()) {
|
|
continue;
|
|
}
|
|
const ParsedSvg* svg = LoadSvg(cmd.svgPath);
|
|
if (!svg || svg->circles.empty() || svg->viewWidth <= 0.0f || svg->viewHeight <= 0.0f) {
|
|
continue;
|
|
}
|
|
|
|
GuiCommand::RectData clippedTarget = IntersectRect(cmd.rect, activeClip);
|
|
if (!RectHasArea(clippedTarget)) {
|
|
continue;
|
|
}
|
|
|
|
float scaleX = clippedTarget.width / svg->viewWidth;
|
|
float scaleY = clippedTarget.height / svg->viewHeight;
|
|
float scale = std::min(scaleX, scaleY);
|
|
|
|
for (const auto& circle : svg->circles) {
|
|
float cx = clippedTarget.x + circle.cx * scaleX;
|
|
float cy = clippedTarget.y + circle.cy * scaleY;
|
|
float radius = circle.r * scale;
|
|
if (radius <= 0.0f) {
|
|
continue;
|
|
}
|
|
|
|
GuiColor color = circle.color;
|
|
if (cmd.svgTint.a > 0.0f) {
|
|
color.r *= cmd.svgTint.r;
|
|
color.g *= cmd.svgTint.g;
|
|
color.b *= cmd.svgTint.b;
|
|
color.a *= cmd.svgTint.a;
|
|
}
|
|
|
|
int segments = std::max(12, static_cast<int>(radius * 0.25f));
|
|
const float twoPi = 6.283185307179586f;
|
|
ClipPoint center{cx, cy};
|
|
|
|
for (int i = 0; i < segments; ++i) {
|
|
float angle0 = twoPi * static_cast<float>(i) / static_cast<float>(segments);
|
|
float angle1 = twoPi * static_cast<float>(i + 1) / static_cast<float>(segments);
|
|
ClipPoint p0{cx + std::cos(angle0) * radius, cy + std::sin(angle0) * radius};
|
|
ClipPoint p1{cx + std::cos(angle1) * radius, cy + std::sin(angle1) * radius};
|
|
addClippedPolygon({center, p0, p1}, color, activeClip);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::vector<uint8_t>& GuiRenderer::LoadShaderBytes(const std::filesystem::path& path,
|
|
VkShaderStageFlagBits stage) {
|
|
const std::string key = path.string();
|
|
auto cached = shaderSpirvCache_.find(key);
|
|
if (cached != shaderSpirvCache_.end()) {
|
|
if (logger_) {
|
|
logger_->Trace("GuiRenderer", "LoadShaderBytes",
|
|
"cacheHit=true, path=" + key +
|
|
", bytes=" + std::to_string(cached->second.size()));
|
|
}
|
|
return cached->second;
|
|
}
|
|
|
|
std::vector<uint8_t> shaderBytes = ReadShaderFile(path, stage, logger_.get());
|
|
auto inserted = shaderSpirvCache_.emplace(key, std::move(shaderBytes));
|
|
if (logger_) {
|
|
logger_->Trace("GuiRenderer", "LoadShaderBytes",
|
|
"cacheHit=false, path=" + key +
|
|
", bytes=" + std::to_string(inserted.first->second.size()));
|
|
}
|
|
return inserted.first->second;
|
|
}
|
|
|
|
const std::vector<uint8_t>& GuiRenderer::LoadShaderBytes(const std::string& cacheKey,
|
|
const std::string& source,
|
|
VkShaderStageFlagBits stage) {
|
|
auto cached = shaderSpirvCache_.find(cacheKey);
|
|
if (cached != shaderSpirvCache_.end()) {
|
|
if (logger_) {
|
|
logger_->Trace("GuiRenderer", "LoadShaderBytes",
|
|
"cacheHit=true, key=" + cacheKey +
|
|
", bytes=" + std::to_string(cached->second.size()));
|
|
}
|
|
return cached->second;
|
|
}
|
|
|
|
std::vector<uint8_t> shaderBytes = ReadShaderSource(source, stage, cacheKey, logger_.get());
|
|
auto inserted = shaderSpirvCache_.emplace(cacheKey, std::move(shaderBytes));
|
|
if (logger_) {
|
|
logger_->Trace("GuiRenderer", "LoadShaderBytes",
|
|
"cacheHit=false, key=" + cacheKey +
|
|
", bytes=" + std::to_string(inserted.first->second.size()));
|
|
}
|
|
return inserted.first->second;
|
|
}
|
|
|
|
void GuiRenderer::CreatePipeline(VkRenderPass renderPass, VkExtent2D extent) {
|
|
// Load shader modules
|
|
const std::string vertexLabel = "inline:gui_2d.vert";
|
|
const std::string fragmentLabel = "inline:gui_2d.frag";
|
|
if (logger_) {
|
|
logger_->Trace("GuiRenderer", "CreatePipeline",
|
|
"renderPassIsNull=" + std::string(renderPass == VK_NULL_HANDLE ? "true" : "false") +
|
|
", extent=" + std::to_string(extent.width) + "x" + std::to_string(extent.height) +
|
|
", vertexShader=" + vertexLabel +
|
|
", fragmentShader=" + fragmentLabel);
|
|
}
|
|
|
|
const auto& vertShaderCode = LoadShaderBytes(vertexLabel, kGuiVertexSource,
|
|
VK_SHADER_STAGE_VERTEX_BIT);
|
|
const auto& fragShaderCode = LoadShaderBytes(fragmentLabel, kGuiFragmentSource,
|
|
VK_SHADER_STAGE_FRAGMENT_BIT);
|
|
|
|
VkShaderModuleCreateInfo vertModuleInfo{};
|
|
vertModuleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
|
vertModuleInfo.codeSize = vertShaderCode.size();
|
|
vertModuleInfo.pCode = reinterpret_cast<const uint32_t*>(vertShaderCode.data());
|
|
|
|
if (vkCreateShaderModule(device_, &vertModuleInfo, nullptr, &vertShaderModule_) != VK_SUCCESS) {
|
|
throw std::runtime_error("Failed to create vertex shader module for GUI");
|
|
}
|
|
|
|
VkShaderModuleCreateInfo fragModuleInfo{};
|
|
fragModuleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
|
fragModuleInfo.codeSize = fragShaderCode.size();
|
|
fragModuleInfo.pCode = reinterpret_cast<const uint32_t*>(fragShaderCode.data());
|
|
|
|
if (vkCreateShaderModule(device_, &fragModuleInfo, nullptr, &fragShaderModule_) != VK_SUCCESS) {
|
|
throw std::runtime_error("Failed to create fragment shader module for GUI");
|
|
}
|
|
|
|
// Shader stages
|
|
VkPipelineShaderStageCreateInfo vertStageInfo{};
|
|
vertStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
|
vertStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
|
|
vertStageInfo.module = vertShaderModule_;
|
|
vertStageInfo.pName = "main";
|
|
|
|
VkPipelineShaderStageCreateInfo fragStageInfo{};
|
|
fragStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
|
fragStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
fragStageInfo.module = fragShaderModule_;
|
|
fragStageInfo.pName = "main";
|
|
|
|
VkPipelineShaderStageCreateInfo shaderStages[] = {vertStageInfo, fragStageInfo};
|
|
|
|
// Vertex input - GuiVertex format
|
|
VkVertexInputBindingDescription bindingDesc{};
|
|
bindingDesc.binding = 0;
|
|
bindingDesc.stride = sizeof(GuiVertex);
|
|
bindingDesc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
std::array<VkVertexInputAttributeDescription, 2> attributeDescs{};
|
|
// Position (vec3)
|
|
attributeDescs[0].binding = 0;
|
|
attributeDescs[0].location = 0;
|
|
attributeDescs[0].format = VK_FORMAT_R32G32B32_SFLOAT;
|
|
attributeDescs[0].offset = offsetof(GuiVertex, x);
|
|
|
|
// Color (vec4)
|
|
attributeDescs[1].binding = 0;
|
|
attributeDescs[1].location = 1;
|
|
attributeDescs[1].format = VK_FORMAT_R32G32B32A32_SFLOAT;
|
|
attributeDescs[1].offset = offsetof(GuiVertex, r);
|
|
|
|
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
|
|
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
|
vertexInputInfo.vertexBindingDescriptionCount = 1;
|
|
vertexInputInfo.pVertexBindingDescriptions = &bindingDesc;
|
|
vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(attributeDescs.size());
|
|
vertexInputInfo.pVertexAttributeDescriptions = attributeDescs.data();
|
|
|
|
// Input assembly
|
|
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
|
|
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
|
|
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
|
inputAssembly.primitiveRestartEnable = VK_FALSE;
|
|
|
|
// Viewport and scissor
|
|
VkViewport viewport{};
|
|
viewport.x = 0.0f;
|
|
viewport.y = 0.0f;
|
|
viewport.width = static_cast<float>(extent.width);
|
|
viewport.height = static_cast<float>(extent.height);
|
|
viewport.minDepth = 0.0f;
|
|
viewport.maxDepth = 1.0f;
|
|
|
|
VkRect2D scissor{};
|
|
scissor.offset = {0, 0};
|
|
scissor.extent = extent;
|
|
|
|
VkPipelineViewportStateCreateInfo viewportState{};
|
|
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
|
|
viewportState.viewportCount = 1;
|
|
viewportState.pViewports = &viewport;
|
|
viewportState.scissorCount = 1;
|
|
viewportState.pScissors = &scissor;
|
|
|
|
// Rasterization
|
|
VkPipelineRasterizationStateCreateInfo rasterizer{};
|
|
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
|
|
rasterizer.depthClampEnable = VK_FALSE;
|
|
rasterizer.rasterizerDiscardEnable = VK_FALSE;
|
|
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
|
|
rasterizer.lineWidth = 1.0f;
|
|
rasterizer.cullMode = VK_CULL_MODE_NONE; // No culling for 2D GUI
|
|
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
|
|
rasterizer.depthBiasEnable = VK_FALSE;
|
|
|
|
// Multisampling - enable sample shading for smoother text
|
|
VkPipelineMultisampleStateCreateInfo multisampling{};
|
|
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
|
multisampling.sampleShadingEnable = VK_TRUE; // Enable for smoother rendering
|
|
multisampling.minSampleShading = 0.5f; // Shade at least 50% of samples
|
|
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
// **CRITICAL: Alpha blending for transparency**
|
|
VkPipelineColorBlendAttachmentState colorBlendAttachment{};
|
|
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
|
|
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
|
colorBlendAttachment.blendEnable = VK_TRUE; // Enable blending!
|
|
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
|
|
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
|
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
|
|
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
|
|
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
|
|
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
|
|
|
|
VkPipelineColorBlendStateCreateInfo colorBlending{};
|
|
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
|
|
colorBlending.logicOpEnable = VK_FALSE;
|
|
colorBlending.attachmentCount = 1;
|
|
colorBlending.pAttachments = &colorBlendAttachment;
|
|
|
|
// Depth stencil - disable depth test for 2D GUI overlay
|
|
VkPipelineDepthStencilStateCreateInfo depthStencil{};
|
|
depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
|
|
depthStencil.depthTestEnable = VK_FALSE;
|
|
depthStencil.depthWriteEnable = VK_FALSE;
|
|
depthStencil.stencilTestEnable = VK_FALSE;
|
|
|
|
// Push constants for transformation matrices
|
|
VkPushConstantRange pushConstantRange{};
|
|
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
|
pushConstantRange.offset = 0;
|
|
pushConstantRange.size = sizeof(float) * 32; // 2 mat4s
|
|
|
|
// Pipeline layout
|
|
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
|
|
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
|
pipelineLayoutInfo.pushConstantRangeCount = 1;
|
|
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
|
|
|
|
if (vkCreatePipelineLayout(device_, &pipelineLayoutInfo, nullptr, &pipelineLayout_) != VK_SUCCESS) {
|
|
throw std::runtime_error("Failed to create GUI pipeline layout");
|
|
}
|
|
|
|
// Create graphics pipeline
|
|
VkGraphicsPipelineCreateInfo pipelineInfo{};
|
|
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
|
|
pipelineInfo.stageCount = 2;
|
|
pipelineInfo.pStages = shaderStages;
|
|
pipelineInfo.pVertexInputState = &vertexInputInfo;
|
|
pipelineInfo.pInputAssemblyState = &inputAssembly;
|
|
pipelineInfo.pViewportState = &viewportState;
|
|
pipelineInfo.pRasterizationState = &rasterizer;
|
|
pipelineInfo.pMultisampleState = &multisampling;
|
|
pipelineInfo.pDepthStencilState = &depthStencil;
|
|
pipelineInfo.pColorBlendState = &colorBlending;
|
|
pipelineInfo.layout = pipelineLayout_;
|
|
pipelineInfo.renderPass = renderPass;
|
|
pipelineInfo.subpass = 0;
|
|
|
|
if (vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline_) != VK_SUCCESS) {
|
|
throw std::runtime_error("Failed to create GUI graphics pipeline");
|
|
}
|
|
}
|
|
|
|
void GuiRenderer::CreateVertexAndIndexBuffers(size_t vertexCount, size_t indexCount) {
|
|
// Clean up old buffers
|
|
CleanupBuffers();
|
|
|
|
if (vertexCount == 0 || indexCount == 0) {
|
|
return;
|
|
}
|
|
|
|
// Create vertex buffer
|
|
VkDeviceSize vertexBufferSize = sizeof(GuiVertex) * vertexCount;
|
|
bufferService_->CreateBuffer(
|
|
vertexBufferSize,
|
|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
|
vertexBuffer_,
|
|
vertexMemory_
|
|
);
|
|
|
|
// Upload vertex data
|
|
void* vertexData;
|
|
vkMapMemory(device_, vertexMemory_, 0, vertexBufferSize, 0, &vertexData);
|
|
std::memcpy(vertexData, vertices_.data(), vertexBufferSize);
|
|
vkUnmapMemory(device_, vertexMemory_);
|
|
|
|
// Create index buffer
|
|
VkDeviceSize indexBufferSize = sizeof(uint32_t) * indexCount;
|
|
bufferService_->CreateBuffer(
|
|
indexBufferSize,
|
|
VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
|
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
|
indexBuffer_,
|
|
indexMemory_
|
|
);
|
|
|
|
// Upload index data
|
|
void* indexData;
|
|
vkMapMemory(device_, indexMemory_, 0, indexBufferSize, 0, &indexData);
|
|
std::memcpy(indexData, indices_.data(), indexBufferSize);
|
|
vkUnmapMemory(device_, indexMemory_);
|
|
}
|
|
|
|
void GuiRenderer::CleanupPipeline() {
|
|
if (pipeline_ != VK_NULL_HANDLE) {
|
|
vkDestroyPipeline(device_, pipeline_, nullptr);
|
|
pipeline_ = VK_NULL_HANDLE;
|
|
}
|
|
if (pipelineLayout_ != VK_NULL_HANDLE) {
|
|
vkDestroyPipelineLayout(device_, pipelineLayout_, nullptr);
|
|
pipelineLayout_ = VK_NULL_HANDLE;
|
|
}
|
|
if (vertShaderModule_ != VK_NULL_HANDLE) {
|
|
vkDestroyShaderModule(device_, vertShaderModule_, nullptr);
|
|
vertShaderModule_ = VK_NULL_HANDLE;
|
|
}
|
|
if (fragShaderModule_ != VK_NULL_HANDLE) {
|
|
vkDestroyShaderModule(device_, fragShaderModule_, nullptr);
|
|
fragShaderModule_ = VK_NULL_HANDLE;
|
|
}
|
|
}
|
|
|
|
void GuiRenderer::CleanupBuffers() {
|
|
if (vertexBuffer_ != VK_NULL_HANDLE) {
|
|
vkDestroyBuffer(device_, vertexBuffer_, nullptr);
|
|
vertexBuffer_ = VK_NULL_HANDLE;
|
|
}
|
|
if (vertexMemory_ != VK_NULL_HANDLE) {
|
|
vkFreeMemory(device_, vertexMemory_, nullptr);
|
|
vertexMemory_ = VK_NULL_HANDLE;
|
|
}
|
|
if (indexBuffer_ != VK_NULL_HANDLE) {
|
|
vkDestroyBuffer(device_, indexBuffer_, nullptr);
|
|
indexBuffer_ = VK_NULL_HANDLE;
|
|
}
|
|
if (indexMemory_ != VK_NULL_HANDLE) {
|
|
vkFreeMemory(device_, indexMemory_, nullptr);
|
|
indexMemory_ = VK_NULL_HANDLE;
|
|
}
|
|
}
|
|
|
|
void GuiRenderer::UpdateFormat(VkFormat format) {
|
|
if (swapchainFormat_ == format) {
|
|
return;
|
|
}
|
|
swapchainFormat_ = format;
|
|
}
|
|
|
|
const ParsedSvg* GuiRenderer::LoadSvg(const std::string& relativePath) {
|
|
auto it = svgCache_.find(relativePath);
|
|
if (it != svgCache_.end()) {
|
|
return &it->second;
|
|
}
|
|
std::filesystem::path path = scriptDirectory_ / relativePath;
|
|
try {
|
|
ParsedSvg parsed = ParseSvgFile(path);
|
|
auto inserted = svgCache_.emplace(relativePath, std::move(parsed));
|
|
return &inserted.first->second;
|
|
} catch (...) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
} // namespace sdl3cpp::services::impl
|