refactor: Restructure GUI renderer implementation and update service dependencies

This commit is contained in:
2026-01-04 23:59:09 +00:00
parent c727d7ff38
commit bb497aaae2
7 changed files with 38 additions and 43 deletions

View File

@@ -0,0 +1,567 @@
#include "gui_renderer.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cctype>
#include <cstdio>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
#include "../../third_party/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;
}
int ClampToRange(int value, int minimum, int maximum) {
return std::min(std::max(value, minimum), maximum);
}
} // namespace
class GuiRenderer::Canvas {
public:
using RectData = GuiCommand::RectData;
void Resize(uint32_t width, uint32_t height) {
width_ = width;
height_ = height;
pixels_.assign(static_cast<size_t>(width_) * static_cast<size_t>(height_) * 4, 0);
clipStack_.clear();
clipStack_.push_back({0.0f, 0.0f, static_cast<float>(width_), static_cast<float>(height_)});
}
void Clear() {
std::fill(pixels_.begin(), pixels_.end(), 0);
clipStack_.clear();
clipStack_.push_back({0.0f, 0.0f, static_cast<float>(width_), static_cast<float>(height_)});
}
void PushClip(const RectData& rect) {
clipStack_.push_back(rect);
}
void PopClip() {
if (clipStack_.size() > 1) {
clipStack_.pop_back();
}
}
void FillRect(const RectData& rect, const GuiColor& fillColor,
const GuiColor& borderColor, float borderWidth) {
DrawFilledRect(rect, fillColor);
if (borderWidth > 0.0f && borderColor.a > 0.0f) {
DrawFilledRect({rect.x, rect.y, rect.width, borderWidth}, borderColor);
DrawFilledRect({rect.x, rect.y + rect.height - borderWidth, rect.width, borderWidth}, borderColor);
DrawFilledRect({rect.x, rect.y + borderWidth, borderWidth, rect.height - borderWidth * 2.0f}, borderColor);
DrawFilledRect({rect.x + rect.width - borderWidth, rect.y + borderWidth, borderWidth,
rect.height - borderWidth * 2.0f}, borderColor);
}
}
void DrawText(const std::string& text, const GuiColor& color, const RectData& bounds,
const std::string& alignX, const std::string& alignY, float fontSize) {
if (text.empty() || width_ == 0 || height_ == 0) {
return;
}
float scale = std::max(1.0f, fontSize / 8.0f);
float glyphWidth = 8.0f * scale;
float glyphHeight = 8.0f * scale;
float textWidth = glyphWidth * static_cast<float>(text.size());
float x = bounds.x;
if (alignX == "center") {
x += (bounds.width - textWidth) * 0.5f;
} else if (alignX == "right") {
x += bounds.width - textWidth;
}
float y = bounds.y;
if (alignY == "center") {
y += (bounds.height - glyphHeight) * 0.5f;
} else if (alignY == "bottom") {
y += bounds.height - glyphHeight;
}
for (size_t i = 0; i < text.size(); ++i) {
unsigned char code = static_cast<unsigned char>(text[i]);
if (code >= 128) {
continue;
}
float glyphX = x + glyphWidth * static_cast<float>(i);
for (int row = 0; row < 8; ++row) {
uint8_t pattern = static_cast<uint8_t>(font8x8_basic[code][row]);
for (int col = 0; col < 8; ++col) {
if ((pattern & (1 << col)) == 0) {
continue;
}
RectData pixelRect{
glyphX + static_cast<float>(col) * scale,
y + static_cast<float>(row) * scale,
scale,
scale,
};
DrawFilledRect(pixelRect, color);
}
}
}
}
void DrawSvg(const ParsedSvg& svg, const RectData& target, const GuiColor& tint) {
if (svg.circles.empty() || svg.viewWidth <= 0.0f || svg.viewHeight <= 0.0f || width_ == 0 ||
height_ == 0) {
return;
}
RectData clipped = ClipRect(target);
if (clipped.width <= 0.0f || clipped.height <= 0.0f) {
return;
}
float scaleX = clipped.width / svg.viewWidth;
float scaleY = clipped.height / svg.viewHeight;
float scale = std::min(scaleX, scaleY);
for (const auto& circle : svg.circles) {
float cx = clipped.x + circle.cx * scaleX;
float cy = clipped.y + circle.cy * scaleY;
float radius = circle.r * scale;
GuiColor color = circle.color;
if (tint.a > 0.0f) {
color.r *= tint.r;
color.g *= tint.g;
color.b *= tint.b;
color.a *= tint.a;
}
int yStart = ClampToRange(static_cast<int>(std::floor(cy - radius)), 0, static_cast<int>(height_));
int yEnd = ClampToRange(static_cast<int>(std::ceil(cy + radius)), 0, static_cast<int>(height_));
for (int row = yStart; row < yEnd; ++row) {
float dy = (static_cast<float>(row) + 0.5f) - cy;
float horizontalSpan = radius * radius - dy * dy;
if (horizontalSpan <= 0.0f) {
continue;
}
float span = std::sqrt(horizontalSpan);
RectData slice{
cx - span,
static_cast<float>(row),
2.0f * span,
1.0f,
};
DrawFilledRect(slice, color);
}
}
}
const std::vector<uint8_t>& Pixels() const {
return pixels_;
}
private:
RectData ClipRect(const RectData& rect) const {
RectData clipped = rect;
for (const auto& entry : clipStack_) {
clipped = IntersectRect(clipped, entry);
}
return clipped;
}
void DrawFilledRect(const RectData& rect, const GuiColor& color) {
if (rect.width <= 0.0f || rect.height <= 0.0f) {
return;
}
RectData clipped = ClipRect(rect);
if (clipped.width <= 0.0f || clipped.height <= 0.0f) {
return;
}
int startX = ClampToRange(static_cast<int>(std::floor(clipped.x)), 0, static_cast<int>(width_));
int startY = ClampToRange(static_cast<int>(std::floor(clipped.y)), 0, static_cast<int>(height_));
int endX = ClampToRange(static_cast<int>(std::ceil(clipped.x + clipped.width)), 0, static_cast<int>(width_));
int endY = ClampToRange(static_cast<int>(std::ceil(clipped.y + clipped.height)), 0, static_cast<int>(height_));
for (int y = startY; y < endY; ++y) {
for (int x = startX; x < endX; ++x) {
BlendPixel(x, y, color);
}
}
}
void BlendPixel(int x, int y, const GuiColor& color) {
size_t index = (static_cast<size_t>(y) * width_ + static_cast<size_t>(x)) * 4;
auto clampByte = [](float value) -> uint8_t {
return static_cast<uint8_t>(std::clamp(value, 0.0f, 1.0f) * 255.0f);
};
float destR = pixels_[index] / 255.0f;
float destG = pixels_[index + 1] / 255.0f;
float destB = pixels_[index + 2] / 255.0f;
float destA = pixels_[index + 3] / 255.0f;
float srcA = std::clamp(color.a, 0.0f, 1.0f);
float invSrc = 1.0f - srcA;
float outR = color.r * srcA + destR * invSrc;
float outG = color.g * srcA + destG * invSrc;
float outB = color.b * srcA + destB * invSrc;
float outA = srcA + destA * invSrc;
pixels_[index] = clampByte(outR);
pixels_[index + 1] = clampByte(outG);
pixels_[index + 2] = clampByte(outB);
pixels_[index + 3] = clampByte(outA);
}
uint32_t width_ = 0;
uint32_t height_ = 0;
std::vector<uint8_t> pixels_;
std::vector<RectData> clipStack_;
};
GuiRenderer::GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFormat swapchainFormat,
const std::filesystem::path& scriptDirectory,
std::shared_ptr<IBufferService> bufferService)
: device_(device),
physicalDevice_(physicalDevice),
swapchainFormat_(swapchainFormat),
scriptDirectory_(scriptDirectory),
canvas_(std::make_unique<Canvas>()),
bufferService_(std::move(bufferService)) {
}
GuiRenderer::~GuiRenderer() {
DestroyStagingBuffer();
}
bool GuiRenderer::IsReady() const {
return canvasWidth_ > 0 && canvasHeight_ > 0 && stagingBuffer_ != VK_NULL_HANDLE;
}
void GuiRenderer::Prepare(const std::vector<GuiCommand>& commands, uint32_t width,
uint32_t height) {
if (width == 0 || height == 0 || !canvas_) {
return;
}
EnsureCanvas(width, height);
canvas_->Clear();
for (const auto& command : commands) {
switch (command.type) {
case GuiCommand::Type::Rect:
canvas_->FillRect(command.rect, command.color, command.borderColor, command.borderWidth);
break;
case GuiCommand::Type::Text: {
if (command.hasClipRect) {
canvas_->PushClip(command.clipRect);
}
if (command.hasBounds) {
canvas_->DrawText(command.text, command.color, command.bounds, command.alignX,
command.alignY, command.fontSize);
} else {
GuiCommand::RectData fallback{
command.rect.x, command.rect.y,
command.fontSize * static_cast<float>(std::max<size_t>(1, command.text.size())), command.fontSize};
canvas_->DrawText(command.text, command.color, fallback, command.alignX,
command.alignY, command.fontSize);
}
if (command.hasClipRect) {
canvas_->PopClip();
}
break;
}
case GuiCommand::Type::ClipPush:
canvas_->PushClip(command.rect);
break;
case GuiCommand::Type::ClipPop:
canvas_->PopClip();
break;
case GuiCommand::Type::Svg:
if (command.svgPath.empty()) {
break;
}
if (const ParsedSvg* svg = LoadSvg(command.svgPath)) {
canvas_->DrawSvg(*svg, command.rect, command.svgTint);
}
break;
}
}
UpdateStagingBuffer();
}
void GuiRenderer::BlitToSwapchain(VkCommandBuffer commandBuffer, VkImage image) {
if (!IsReady()) {
return;
}
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.image = image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
VkBufferImageCopy region{};
region.bufferOffset = 0;
region.bufferRowLength = 0;
region.bufferImageHeight = 0;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageOffset = {0, 0, 0};
region.imageExtent = {canvasWidth_, canvasHeight_, 1};
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer_, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
&region);
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = 0;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0,
0, nullptr, 0, nullptr, 1, &barrier);
}
void GuiRenderer::Resize(uint32_t width, uint32_t height, VkFormat format) {
if (width == canvasWidth_ && height == canvasHeight_ && format == swapchainFormat_) {
return;
}
UpdateFormat(format);
EnsureCanvas(width, height);
}
void GuiRenderer::EnsureCanvas(uint32_t width, uint32_t height) {
if (width == canvasWidth_ && height == canvasHeight_) {
return;
}
canvasWidth_ = width;
canvasHeight_ = height;
if (canvas_) {
canvas_->Resize(width, height);
}
size_t bufferSize = static_cast<size_t>(canvasWidth_) * canvasHeight_ * 4;
CreateStagingBuffer(bufferSize);
}
void GuiRenderer::UpdateStagingBuffer() {
if (!stagingMapped_ || !canvas_) {
return;
}
const auto& pixels = canvas_->Pixels();
size_t pixelCount = static_cast<size_t>(canvasWidth_) * canvasHeight_;
uint8_t* dest = reinterpret_cast<uint8_t*>(stagingMapped_);
for (size_t i = 0; i < pixelCount; ++i) {
size_t offset = i * 4;
uint8_t r = pixels[offset];
uint8_t g = pixels[offset + 1];
uint8_t b = pixels[offset + 2];
uint8_t a = pixels[offset + 3];
switch (swapchainFormat_) {
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SRGB:
dest[offset] = b;
dest[offset + 1] = g;
dest[offset + 2] = r;
dest[offset + 3] = a;
break;
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
default:
dest[offset] = r;
dest[offset + 1] = g;
dest[offset + 2] = b;
dest[offset + 3] = a;
break;
}
}
}
void GuiRenderer::CreateStagingBuffer(size_t size) {
DestroyStagingBuffer();
if (size == 0) {
return;
}
if (!bufferService_) {
throw std::runtime_error("Buffer service not available for GUI staging buffer");
}
bufferService_->CreateBuffer(static_cast<VkDeviceSize>(size),
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
stagingBuffer_, stagingMemory_);
stagingSize_ = size;
vkMapMemory(device_, stagingMemory_, 0, stagingSize_, 0, &stagingMapped_);
}
void GuiRenderer::DestroyStagingBuffer() {
if (stagingMapped_) {
vkUnmapMemory(device_, stagingMemory_);
stagingMapped_ = nullptr;
}
if (stagingBuffer_ != VK_NULL_HANDLE) {
vkDestroyBuffer(device_, stagingBuffer_, nullptr);
stagingBuffer_ = VK_NULL_HANDLE;
}
if (stagingMemory_ != VK_NULL_HANDLE) {
vkFreeMemory(device_, stagingMemory_, nullptr);
stagingMemory_ = VK_NULL_HANDLE;
}
stagingSize_ = 0;
}
void GuiRenderer::UpdateFormat(VkFormat format) {
if (swapchainFormat_ == format) {
return;
}
swapchainFormat_ = format;
DestroyStagingBuffer();
if (canvasWidth_ > 0 && canvasHeight_ > 0) {
size_t bufferSize = static_cast<size_t>(canvasWidth_) * canvasHeight_ * 4;
CreateStagingBuffer(bufferSize);
}
}
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