feat: Implement Vulkan debug messenger and validation layers

- Added Vulkan debug callback and messenger setup to VulkanDeviceService.
- Integrated validation layers into Vulkan instance creation.
- Enhanced VulkanGraphicsBackend with methods to recreate swapchain and wait for GPU idle.
- Updated VulkanGuiService to include render pass in resize method.
- Removed deprecated VulkanGraphicsBackend_old.cpp file.
- Refactored interfaces to support new methods for swapchain management and rendering.
This commit is contained in:
2026-01-05 20:13:58 +00:00
parent 4a9410e6c7
commit be181b177f
24 changed files with 628 additions and 2007 deletions

View File

@@ -78,7 +78,18 @@ void GraphicsService::RecreateSwapchain() {
throw std::runtime_error("Graphics service not initialized");
}
// TODO: Implement swapchain recreation via backend
auto size = windowService_ ? windowService_->GetSize() : std::pair<uint32_t, uint32_t>{0, 0};
if (logger_) {
logger_->Trace("GraphicsService", "RecreateSwapchain",
"windowSize=" + std::to_string(size.first) + "x" +
std::to_string(size.second));
}
if (size.first == 0 || size.second == 0) {
logger_->Warn("GraphicsService::RecreateSwapchain: Skipping recreation for zero-size window");
return;
}
backend_->RecreateSwapchain(size.first, size.second);
}
void GraphicsService::LoadShaders(const std::unordered_map<std::string, ShaderPaths>& shaders) {
@@ -173,7 +184,7 @@ void GraphicsService::WaitIdle() {
return;
}
// TODO: Implement via backend
backend_->WaitIdle();
}
GraphicsDeviceHandle GraphicsService::GetDevice() const {
@@ -193,8 +204,7 @@ GraphicsDeviceHandle GraphicsService::GetPhysicalDevice() const {
return nullptr;
}
// TODO: Return physical device from backend
return nullptr;
return backend_->GetPhysicalDevice();
}
std::pair<uint32_t, uint32_t> GraphicsService::GetSwapchainExtent() const {
@@ -204,8 +214,7 @@ std::pair<uint32_t, uint32_t> GraphicsService::GetSwapchainExtent() const {
return {0, 0};
}
// TODO: Return extent from backend
return {800, 600}; // Placeholder
return backend_->GetSwapchainExtent();
}
uint32_t GraphicsService::GetSwapchainFormat() const {
@@ -215,8 +224,7 @@ uint32_t GraphicsService::GetSwapchainFormat() const {
return 0;
}
// TODO: Return format from backend
return 0; // Placeholder
return backend_->GetSwapchainFormat();
}
void* GraphicsService::GetCurrentCommandBuffer() const {
@@ -226,8 +234,7 @@ void* GraphicsService::GetCurrentCommandBuffer() const {
return nullptr;
}
// TODO: Return command buffer from backend
return nullptr;
return backend_->GetCurrentCommandBuffer();
}
void* GraphicsService::GetGraphicsQueue() const {
@@ -237,8 +244,7 @@ void* GraphicsService::GetGraphicsQueue() const {
return nullptr;
}
// TODO: Return queue from backend
return nullptr;
return backend_->GetGraphicsQueue();
}
} // namespace sdl3cpp::services::impl

View File

@@ -137,6 +137,96 @@ ParsedSvg ParseSvgFile(const std::filesystem::path& path) {
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;
}
std::vector<uint8_t> ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
@@ -231,13 +321,15 @@ void GuiRenderer::RenderToSwapchain(VkCommandBuffer commandBuffer, VkRenderPass
vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(indices_.size()), 1, 0, 0, 0);
}
void GuiRenderer::Resize(uint32_t width, uint32_t height, VkFormat format) {
if (width == viewportWidth_ && height == viewportHeight_ && format == swapchainFormat_) {
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) {
@@ -258,30 +350,90 @@ void GuiRenderer::GenerateGuiGeometry(const std::vector<GuiCommand>& commands, u
};
};
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) {
// Generate a quad (2 triangles) for the rectangle
auto [x0, y0] = toNDC(cmd.rect.x, cmd.rect.y);
auto [x1, y1] = toNDC(cmd.rect.x + cmd.rect.width, cmd.rect.y + cmd.rect.height);
uint32_t baseIndex = static_cast<uint32_t>(vertices_.size());
// Add 4 vertices for the quad
vertices_.push_back({x0, y0, 0.0f, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a});
vertices_.push_back({x1, y0, 0.0f, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a});
vertices_.push_back({x1, y1, 0.0f, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a});
vertices_.push_back({x0, y1, 0.0f, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a});
// Add 6 indices for 2 triangles
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);
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() || !cmd.hasBounds) {
if (cmd.text.empty()) {
continue;
}
@@ -295,19 +447,37 @@ void GuiRenderer::GenerateGuiGeometry(const std::vector<GuiCommand>& commands, u
// Calculate text position based on alignment
// Text commands use bounds field, not rect field
float startX = cmd.bounds.x;
float startY = cmd.bounds.y;
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 += (cmd.bounds.width - textWidth) * 0.5f;
startX += (textBounds.width - textWidth) * 0.5f;
} else if (cmd.alignX == "right") {
startX += cmd.bounds.width - textWidth;
startX += textBounds.width - textWidth;
}
if (cmd.alignY == "center") {
startY += (cmd.bounds.height - charHeight) * 0.5f;
startY += (textBounds.height - charHeight) * 0.5f;
} else if (cmd.alignY == "bottom") {
startY += cmd.bounds.height - charHeight;
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
@@ -333,32 +503,61 @@ void GuiRenderer::GenerateGuiGeometry(const std::vector<GuiCommand>& commands, u
float pw = pixelWidth;
float ph = pixelHeight;
auto [px0, py0] = toNDC(px, py);
auto [px1, py1] = toNDC(px + pw, py + ph);
uint32_t baseIndex = static_cast<uint32_t>(vertices_.size());
// Add 4 vertices for the pixel quad
vertices_.push_back({px0, py0, 0.0f, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a});
vertices_.push_back({px1, py0, 0.0f, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a});
vertices_.push_back({px1, py1, 0.0f, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a});
vertices_.push_back({px0, py1, 0.0f, cmd.color.r, cmd.color.g, cmd.color.b, cmd.color.a});
// Add 6 indices for 2 triangles
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);
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);
}
}
}
// TODO: Implement SVG, ClipPush, ClipPop command types
}
}

View File

@@ -1,596 +0,0 @@
#include "gui_renderer.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iterator>
#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;
}
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),
bufferService_(std::move(bufferService)) {
}
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 || !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;
}
// Check if GUI canvas has any visible pixels (non-zero alpha)
// If all pixels are transparent, skip the blit entirely to avoid overwriting the scene
const auto& pixels = canvas_->Pixels();
size_t pixelCount = static_cast<size_t>(canvasWidth_) * canvasHeight_;
bool hasVisiblePixels = false;
for (size_t i = 0; i < pixelCount; ++i) {
if (pixels[i * 4 + 3] > 0) { // Check alpha channel
hasVisiblePixels = true;
break;
}
}
if (!hasVisiblePixels) {
// GUI is fully transparent, don't blit to avoid black overlay
return;
}
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
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_);
// Clear destination to fully transparent so pixels with alpha=0 don't overwrite the scene
std::memset(dest, 0, pixelCount * 4);
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];
// Skip fully transparent pixels to avoid overwriting the 3D scene
if (a == 0) {
continue;
}
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

View File

@@ -44,7 +44,7 @@ public:
void Prepare(const std::vector<GuiCommand>& commands, uint32_t width,
uint32_t height);
void RenderToSwapchain(VkCommandBuffer commandBuffer, VkRenderPass renderPass);
void Resize(uint32_t width, uint32_t height, VkFormat format);
void Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass);
bool IsReady() const;
private:

View File

@@ -1,635 +0,0 @@
#include "gui_renderer.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iterator>
#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;
}
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,
VkRenderPass renderPass, const std::filesystem::path& scriptDirectory,
std::shared_ptr<IBufferService> bufferService)
: device_(device),
physicalDevice_(physicalDevice),
swapchainFormat_(swapchainFormat),
renderPass_(renderPass),
scriptDirectory_(scriptDirectory),
bufferService_(std::move(bufferService)) {
}
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 || !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;
}
// Check if GUI canvas has any visible pixels (non-zero alpha)
// If all pixels are transparent, skip the blit entirely to avoid overwriting the scene
const auto& pixels = canvas_->Pixels();
size_t pixelCount = static_cast<size_t>(canvasWidth_) * canvasHeight_;
bool hasVisiblePixels = false;
for (size_t i = 0; i < pixelCount; ++i) {
if (pixels[i * 4 + 3] > 0) { // Check alpha channel
hasVisiblePixels = true;
break;
}
}
if (!hasVisiblePixels) {
// GUI is fully transparent, don't blit to avoid black overlay
return;
}
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
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_);
// Clear destination to fully transparent so pixels with alpha=0 don't overwrite the scene
std::memset(dest, 0, pixelCount * 4);
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];
// Skip fully transparent pixels to avoid overwriting the 3D scene
if (a == 0) {
continue;
}
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);
}
}
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;
}
}
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

View File

@@ -60,17 +60,18 @@ void GuiRendererService::RenderToSwapchain(VkCommandBuffer commandBuffer, VkImag
renderer_->RenderToSwapchain(commandBuffer, VK_NULL_HANDLE);
}
void GuiRendererService::Resize(uint32_t width, uint32_t height, VkFormat format) {
void GuiRendererService::Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass) {
if (logger_) {
logger_->Trace("GuiRendererService", "Resize",
"width=" + std::to_string(width) +
", height=" + std::to_string(height) +
", format=" + std::to_string(static_cast<uint32_t>(format)));
", format=" + std::to_string(static_cast<uint32_t>(format)) +
", renderPassIsNull=" + std::string(renderPass == VK_NULL_HANDLE ? "true" : "false"));
}
if (!renderer_) {
return;
}
renderer_->Resize(width, height, format);
renderer_->Resize(width, height, format, renderPass);
}
void GuiRendererService::Shutdown() noexcept {

View File

@@ -27,7 +27,7 @@ public:
void RenderToSwapchain(VkCommandBuffer commandBuffer, VkImage image) override;
void Resize(uint32_t width, uint32_t height, VkFormat format) override;
void Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass) override;
void Shutdown() noexcept override;

View File

@@ -31,7 +31,9 @@ GxmGraphicsBackend::GxmGraphicsBackend()
initialized_(false), vdmRingBuffer_(nullptr), vertexRingBuffer_(nullptr),
fragmentRingBuffer_(nullptr), fragmentUsseRingBuffer_(nullptr),
vdmRingBufferUid_(0), vertexRingBufferUid_(0), fragmentRingBufferUid_(0),
fragmentUsseRingBufferUid_(0) {
fragmentUsseRingBufferUid_(0),
displayWidth_(DISPLAY_WIDTH),
displayHeight_(DISPLAY_HEIGHT) {
}
GxmGraphicsBackend::~GxmGraphicsBackend() {
@@ -252,6 +254,16 @@ void GxmGraphicsBackend::Shutdown() {
initialized_ = false;
}
void GxmGraphicsBackend::RecreateSwapchain(uint32_t width, uint32_t height) {
std::cout << "GXM: Swapchain recreation not supported on Vita (" << width << "x" << height << ")" << std::endl;
}
void GxmGraphicsBackend::WaitIdle() {
if (context_) {
sceGxmFinish(context_);
}
}
GraphicsDeviceHandle GxmGraphicsBackend::CreateDevice() {
std::cout << "GXM: Creating device handle" << std::endl;
return static_cast<GraphicsDeviceHandle>(context_);
@@ -496,6 +508,26 @@ void GxmGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHandl
}
}
GraphicsDeviceHandle GxmGraphicsBackend::GetPhysicalDevice() const {
return nullptr;
}
std::pair<uint32_t, uint32_t> GxmGraphicsBackend::GetSwapchainExtent() const {
return {displayWidth_, displayHeight_};
}
uint32_t GxmGraphicsBackend::GetSwapchainFormat() const {
return 0;
}
void* GxmGraphicsBackend::GetCurrentCommandBuffer() const {
return nullptr;
}
void* GxmGraphicsBackend::GetGraphicsQueue() const {
return nullptr;
}
// Helper methods
int GxmGraphicsBackend::createDisplayBuffers() {
@@ -599,4 +631,4 @@ void GxmGraphicsBackend::destroyShaderPrograms() {
fragmentShaderIds_.clear();
}
} // namespace sdl3cpp::services::impl
} // namespace sdl3cpp::services::impl

View File

@@ -20,6 +20,8 @@ public:
void Initialize(void* window, const GraphicsConfig& config) override;
void Shutdown() override;
void RecreateSwapchain(uint32_t width, uint32_t height) override;
void WaitIdle() override;
GraphicsDeviceHandle CreateDevice() override;
void DestroyDevice(GraphicsDeviceHandle device) override;
@@ -40,6 +42,12 @@ public:
GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer,
uint32_t indexCount, const std::array<float, 16>& modelMatrix) override;
GraphicsDeviceHandle GetPhysicalDevice() const override;
std::pair<uint32_t, uint32_t> GetSwapchainExtent() const override;
uint32_t GetSwapchainFormat() const override;
void* GetCurrentCommandBuffer() const override;
void* GetGraphicsQueue() const override;
private:
// GXM-specific members
SceGxmContext* context_;
@@ -69,6 +77,9 @@ private:
SceUID vertexRingBufferUid_;
SceUID fragmentRingBufferUid_;
SceUID fragmentUsseRingBufferUid_;
uint32_t displayWidth_ = 0;
uint32_t displayHeight_ = 0;
// Helper methods
int createDisplayBuffers();
@@ -78,4 +89,4 @@ private:
void destroyShaderPrograms();
};
} // namespace sdl3cpp::services::impl
} // namespace sdl3cpp::services::impl

View File

@@ -297,6 +297,20 @@ VkCommandBuffer RenderCommandService::GetCurrentCommandBuffer() const {
return commandBuffers_[currentFrame_];
}
void RenderCommandService::OnSwapchainRecreated() {
logger_->Trace("RenderCommandService", "OnSwapchainRecreated");
Cleanup();
currentFrame_ = 0;
if (guiRendererService_) {
VkExtent2D extent = swapchainService_->GetSwapchainExtent();
VkFormat format = swapchainService_->GetSwapchainImageFormat();
VkRenderPass renderPass = swapchainService_->GetRenderPass();
guiRendererService_->Resize(extent.width, extent.height, format, renderPass);
}
}
void RenderCommandService::CreateCommandPool() {
logger_->Trace("RenderCommandService", "CreateCommandPool");

View File

@@ -51,6 +51,7 @@ public:
logger_->Trace("RenderCommandService", "GetMaxFramesInFlight");
return maxFramesInFlight_;
}
void OnSwapchainRecreated() override;
// IShutdownable interface
void Shutdown() noexcept override;

View File

@@ -56,7 +56,13 @@ void RenderCoordinatorService::RenderFrame(float time) {
shadersLoaded_ = true;
}
graphicsService_->BeginFrame();
if (!graphicsService_->BeginFrame()) {
if (logger_) {
logger_->Warn("RenderCoordinatorService::RenderFrame: Swapchain out of date during BeginFrame");
}
graphicsService_->RecreateSwapchain();
return;
}
if (guiService_ && guiScriptService_ && guiScriptService_->HasGuiCommands()) {
auto guiCommands = guiScriptService_->LoadGuiCommands();
@@ -100,7 +106,13 @@ void RenderCoordinatorService::RenderFrame(float time) {
graphicsService_->RenderScene(renderCommands, viewProj);
}
graphicsService_->EndFrame();
if (!graphicsService_->EndFrame()) {
if (logger_) {
logger_->Warn("RenderCoordinatorService::RenderFrame: Swapchain out of date during EndFrame");
}
graphicsService_->RecreateSwapchain();
return;
}
if (logger_) {
logger_->Trace("RenderCoordinatorService", "RenderFrame", "", "Exiting");

View File

@@ -1,5 +1,6 @@
#include "swapchain_service.hpp"
#include <algorithm>
#include <limits>
#include <stdexcept>
#include <string>
@@ -393,6 +394,10 @@ VkExtent2D SwapchainService::ChooseExtent(const VkSurfaceCapabilitiesKHR& capabi
", minHeight=" + std::to_string(capabilities.minImageExtent.height) +
", maxWidth=" + std::to_string(capabilities.maxImageExtent.width) +
", maxHeight=" + std::to_string(capabilities.maxImageExtent.height));
if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
return capabilities.currentExtent;
}
return VkExtent2D{
std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width),
std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height)

View File

@@ -1,10 +1,61 @@
#include "vulkan_device_service.hpp"
#include <SDL3/SDL_vulkan.h>
#include <algorithm>
#include <iostream>
#include <set>
#include <stdexcept>
#include <cstring>
namespace {
VKAPI_ATTR VkBool32 VKAPI_CALL DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* pUserData) {
auto* logger = static_cast<sdl3cpp::services::ILogger*>(pUserData);
std::string message = "Vulkan validation: ";
message += pCallbackData && pCallbackData->pMessage ? pCallbackData->pMessage : "Unknown message";
if (logger) {
if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
logger->Error(message);
} else if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) {
logger->Warn(message);
} else {
logger->Debug(message);
}
} else {
std::cerr << message << std::endl;
}
(void)messageType;
return VK_FALSE;
}
VkResult CreateDebugUtilsMessengerEXT(VkInstance instance,
const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkDebugUtilsMessengerEXT* pDebugMessenger) {
auto func = reinterpret_cast<PFN_vkCreateDebugUtilsMessengerEXT>(
vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"));
if (func != nullptr) {
return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
}
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
void DestroyDebugUtilsMessengerEXT(VkInstance instance,
VkDebugUtilsMessengerEXT debugMessenger,
const VkAllocationCallbacks* pAllocator) {
auto func = reinterpret_cast<PFN_vkDestroyDebugUtilsMessengerEXT>(
vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"));
if (func != nullptr) {
func(instance, debugMessenger, pAllocator);
}
}
} // namespace
namespace sdl3cpp::services::impl {
VulkanDeviceService::VulkanDeviceService(std::shared_ptr<ILogger> logger)
@@ -42,6 +93,7 @@ void VulkanDeviceService::Initialize(const std::vector<const char*>& deviceExten
device_ = VK_NULL_HANDLE;
graphicsQueue_ = VK_NULL_HANDLE;
presentQueue_ = VK_NULL_HANDLE;
debugMessenger_ = VK_NULL_HANDLE;
// Get required extensions from SDL
uint32_t extensionCount = 0;
@@ -51,8 +103,16 @@ void VulkanDeviceService::Initialize(const std::vector<const char*>& deviceExten
}
std::vector<const char*> requiredExtensions(extensions, extensions + extensionCount);
if (validationLayersEnabled_) {
auto it = std::find(requiredExtensions.begin(), requiredExtensions.end(),
VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
if (it == requiredExtensions.end()) {
requiredExtensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
}
CreateInstance(requiredExtensions);
SetupDebugMessenger();
logger_->Trace("VulkanDeviceService", "Initialize",
"instanceCreated=" + std::string(instance_ != VK_NULL_HANDLE ? "true" : "false") +
", selectionDeferredUntilSurface=true");
@@ -131,6 +191,13 @@ void VulkanDeviceService::CreateInstance(const std::vector<const char*>& require
createInfo.ppEnabledLayerNames = layerList.data();
createInfo.enabledExtensionCount = static_cast<uint32_t>(requiredExtensions.size());
createInfo.ppEnabledExtensionNames = requiredExtensions.data();
createInfo.pNext = nullptr;
VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
if (validationLayersEnabled_ && !layerList.empty()) {
PopulateDebugMessengerCreateInfo(debugCreateInfo);
createInfo.pNext = &debugCreateInfo;
}
if (vkCreateInstance(&createInfo, nullptr, &instance_) != VK_SUCCESS) {
throw std::runtime_error("Failed to create Vulkan instance");
@@ -313,6 +380,34 @@ void VulkanDeviceService::CreateLogicalDevice() {
vkGetDeviceQueue(device_, indices.presentFamily, 0, &presentQueue_);
}
void VulkanDeviceService::PopulateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) const {
createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = DebugCallback;
createInfo.pUserData = logger_.get();
}
void VulkanDeviceService::SetupDebugMessenger() {
logger_->Trace("VulkanDeviceService", "SetupDebugMessenger",
"validationLayersEnabled=" + std::string(validationLayersEnabled_ ? "true" : "false"));
if (!validationLayersEnabled_) {
return;
}
VkDebugUtilsMessengerCreateInfoEXT createInfo{};
PopulateDebugMessengerCreateInfo(createInfo);
if (CreateDebugUtilsMessengerEXT(instance_, &createInfo, nullptr, &debugMessenger_) != VK_SUCCESS) {
throw std::runtime_error("Failed to set up Vulkan debug messenger");
}
}
void VulkanDeviceService::Shutdown() noexcept {
logger_->Trace("VulkanDeviceService", "Shutdown");
if (device_ != VK_NULL_HANDLE) {
@@ -325,6 +420,11 @@ void VulkanDeviceService::Shutdown() noexcept {
surface_ = VK_NULL_HANDLE;
}
if (debugMessenger_ != VK_NULL_HANDLE) {
DestroyDebugUtilsMessengerEXT(instance_, debugMessenger_, nullptr);
debugMessenger_ = VK_NULL_HANDLE;
}
if (instance_ != VK_NULL_HANDLE) {
vkDestroyInstance(instance_, nullptr);
instance_ = VK_NULL_HANDLE;

View File

@@ -67,12 +67,15 @@ private:
VkDevice device_ = VK_NULL_HANDLE;
VkQueue graphicsQueue_ = VK_NULL_HANDLE;
VkQueue presentQueue_ = VK_NULL_HANDLE;
VkDebugUtilsMessengerEXT debugMessenger_ = VK_NULL_HANDLE;
std::vector<const char*> deviceExtensions_;
bool validationLayersEnabled_ = false;
// Helper methods
void CreateInstance(const std::vector<const char*>& requiredExtensions);
void SetupDebugMessenger();
void PopulateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) const;
void PickPhysicalDevice();
bool IsDeviceSuitable(VkPhysicalDevice device) const;
QueueFamilyIndices FindQueueFamilies(VkPhysicalDevice device) const;

View File

@@ -1,7 +1,9 @@
#include "vulkan_graphics_backend.hpp"
#include "../../core/vertex.hpp"
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <cstring>
#include <stdexcept>
#include <vector>
@@ -54,7 +56,7 @@ void VulkanGraphicsBackend::Initialize(void* window, const GraphicsConfig& confi
// Get window size for swapchain
int width, height;
SDL_GetWindowSize(window_, &width, &height);
SDL_GetWindowSizeInPixels(window_, &width, &height);
// Initialize swapchain
swapchainService_->CreateSwapchain(static_cast<uint32_t>(width), static_cast<uint32_t>(height));
@@ -80,6 +82,33 @@ void VulkanGraphicsBackend::Shutdown() {
initialized_ = false;
}
void VulkanGraphicsBackend::RecreateSwapchain(uint32_t width, uint32_t height) {
logger_->Trace("VulkanGraphicsBackend", "RecreateSwapchain",
"width=" + std::to_string(width) +
", height=" + std::to_string(height));
if (!initialized_) {
return;
}
if (width == 0 || height == 0) {
logger_->Warn("VulkanGraphicsBackend::RecreateSwapchain: Skipping swapchain recreation for zero size");
return;
}
deviceService_->WaitIdle();
swapchainService_->RecreateSwapchain(width, height);
VkExtent2D extent = swapchainService_->GetSwapchainExtent();
pipelineService_->RecreatePipelines(swapchainService_->GetRenderPass(), extent);
renderCommandService_->OnSwapchainRecreated();
}
void VulkanGraphicsBackend::WaitIdle() {
logger_->Trace("VulkanGraphicsBackend", "WaitIdle");
deviceService_->WaitIdle();
}
GraphicsDeviceHandle VulkanGraphicsBackend::CreateDevice() {
logger_->Trace("VulkanGraphicsBackend", "CreateDevice");
// Device is already created in Initialize, just return a handle
@@ -99,10 +128,12 @@ GraphicsPipelineHandle VulkanGraphicsBackend::CreatePipeline(GraphicsDeviceHandl
// Compile pipeline with render pass from swapchain service
// Note: This assumes swapchain service has created the render pass
VkExtent2D extent;
// TODO: Get extent from swapchain service
extent.width = 800; // Temporary
extent.height = 600; // Temporary
VkExtent2D extent = swapchainService_->GetSwapchainExtent();
if (logger_) {
logger_->Trace("VulkanGraphicsBackend", "CreatePipeline",
"swapchainExtent=" + std::to_string(extent.width) + "x" +
std::to_string(extent.height));
}
pipelineService_->CompileAll(swapchainService_->GetRenderPass(), extent);
@@ -124,18 +155,52 @@ void VulkanGraphicsBackend::DestroyPipeline(GraphicsDeviceHandle device, Graphic
GraphicsBufferHandle VulkanGraphicsBackend::CreateVertexBuffer(GraphicsDeviceHandle device, const std::vector<uint8_t>& data) {
logger_->Trace("VulkanGraphicsBackend", "CreateVertexBuffer", "data.size=" + std::to_string(data.size()));
// For now, we'll use the buffer service's existing vertex buffer functionality
// This is a bit of a mismatch - the buffer service expects core::Vertex, but we get raw bytes
// TODO: Extend buffer service to handle raw buffer creation or create a new method
if (data.empty()) {
logger_->Error("VulkanGraphicsBackend::CreateVertexBuffer: No vertex data provided");
return nullptr;
}
if (data.size() % sizeof(core::Vertex) != 0) {
logger_->Error("VulkanGraphicsBackend::CreateVertexBuffer: Vertex data size is not aligned to Vertex");
return nullptr;
}
const size_t vertexCount = data.size() / sizeof(core::Vertex);
if (logger_) {
logger_->Trace("VulkanGraphicsBackend", "CreateVertexBuffer",
"vertexCount=" + std::to_string(vertexCount));
}
std::vector<core::Vertex> vertices(vertexCount);
std::memcpy(vertices.data(), data.data(), data.size());
bufferService_->UploadVertexData(vertices);
// Return a dummy handle for now
return reinterpret_cast<GraphicsBufferHandle>(bufferService_->GetVertexBuffer());
}
GraphicsBufferHandle VulkanGraphicsBackend::CreateIndexBuffer(GraphicsDeviceHandle device, const std::vector<uint8_t>& data) {
logger_->Trace("VulkanGraphicsBackend", "CreateIndexBuffer", "data.size=" + std::to_string(data.size()));
// Similar issue as vertex buffer
if (data.empty()) {
logger_->Error("VulkanGraphicsBackend::CreateIndexBuffer: No index data provided");
return nullptr;
}
if (data.size() % sizeof(uint16_t) != 0) {
logger_->Error("VulkanGraphicsBackend::CreateIndexBuffer: Index data size is not aligned to uint16_t");
return nullptr;
}
const size_t indexCount = data.size() / sizeof(uint16_t);
if (logger_) {
logger_->Trace("VulkanGraphicsBackend", "CreateIndexBuffer",
"indexCount=" + std::to_string(indexCount));
}
std::vector<uint16_t> indices(indexCount);
std::memcpy(indices.data(), data.data(), data.size());
bufferService_->UploadIndexData(indices);
return reinterpret_cast<GraphicsBufferHandle>(bufferService_->GetIndexBuffer());
}
@@ -190,4 +255,30 @@ void VulkanGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHa
frameCommands_.push_back(command);
}
} // namespace sdl3cpp::services::impl
GraphicsDeviceHandle VulkanGraphicsBackend::GetPhysicalDevice() const {
logger_->Trace("VulkanGraphicsBackend", "GetPhysicalDevice");
return reinterpret_cast<GraphicsDeviceHandle>(deviceService_->GetPhysicalDevice());
}
std::pair<uint32_t, uint32_t> VulkanGraphicsBackend::GetSwapchainExtent() const {
logger_->Trace("VulkanGraphicsBackend", "GetSwapchainExtent");
VkExtent2D extent = swapchainService_->GetSwapchainExtent();
return {extent.width, extent.height};
}
uint32_t VulkanGraphicsBackend::GetSwapchainFormat() const {
logger_->Trace("VulkanGraphicsBackend", "GetSwapchainFormat");
return static_cast<uint32_t>(swapchainService_->GetSwapchainImageFormat());
}
void* VulkanGraphicsBackend::GetCurrentCommandBuffer() const {
logger_->Trace("VulkanGraphicsBackend", "GetCurrentCommandBuffer");
return reinterpret_cast<void*>(renderCommandService_->GetCurrentCommandBuffer());
}
void* VulkanGraphicsBackend::GetGraphicsQueue() const {
logger_->Trace("VulkanGraphicsBackend", "GetGraphicsQueue");
return reinterpret_cast<void*>(deviceService_->GetGraphicsQueue());
}
} // namespace sdl3cpp::services::impl

View File

@@ -31,6 +31,8 @@ public:
void Initialize(void* window, const GraphicsConfig& config) override;
void Shutdown() override;
void RecreateSwapchain(uint32_t width, uint32_t height) override;
void WaitIdle() override;
GraphicsDeviceHandle CreateDevice() override;
void DestroyDevice(GraphicsDeviceHandle device) override;
@@ -51,6 +53,12 @@ public:
GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer,
uint32_t indexCount, const std::array<float, 16>& modelMatrix) override;
GraphicsDeviceHandle GetPhysicalDevice() const override;
std::pair<uint32_t, uint32_t> GetSwapchainExtent() const override;
uint32_t GetSwapchainFormat() const override;
void* GetCurrentCommandBuffer() const override;
void* GetGraphicsQueue() const override;
private:
std::shared_ptr<IVulkanDeviceService> deviceService_;
std::shared_ptr<ISwapchainService> swapchainService_;
@@ -67,4 +75,4 @@ private:
std::unordered_map<GraphicsPipelineHandle, std::string> pipelineToShaderKey_;
};
} // namespace sdl3cpp::services::impl
} // namespace sdl3cpp::services::impl

View File

@@ -1,688 +0,0 @@
#include "vulkan_graphics_backend.hpp"
#include <vulkan/vulkan.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <stdexcept>
#include <vector>
#include <array>
#include <set>
#include <algorithm>
#include <fstream>
namespace sdl3cpp::services::impl {
const std::vector<const char*> validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
const std::vector<const char*> deviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() {
return graphicsFamily.has_value() && presentFamily.has_value();
}
};
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
return VK_FALSE;
}
VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) {
auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
if (func != nullptr) {
return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
} else {
return VK_ERROR_EXTENSION_NOT_FOUND;
}
}
void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (func != nullptr) {
func(instance, debugMessenger, pAllocator);
}
}
#ifdef NDEBUG
const bool enableValidationLayers = false;
#else
const bool enableValidationLayers = true;
#endif
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* pUserData) {
std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl;
return VK_FALSE;
}
VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) {
auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
if (func != nullptr) {
return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
} else {
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
}
void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) {
auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (func != nullptr) {
func(instance, debugMessenger, pAllocator);
}
}
VulkanGraphicsBackend::VulkanGraphicsBackend()
: instance_(VK_NULL_HANDLE), physicalDevice_(VK_NULL_HANDLE), device_(VK_NULL_HANDLE),
graphicsQueue_(VK_NULL_HANDLE), surface_(VK_NULL_HANDLE), swapchain_(VK_NULL_HANDLE),
renderPass_(VK_NULL_HANDLE), commandPool_(VK_NULL_HANDLE), currentFrame_(0), initialized_(false) {
}
VulkanGraphicsBackend::~VulkanGraphicsBackend() {
if (initialized_) {
Shutdown();
}
}
void VulkanGraphicsBackend::Initialize(void* window, const GraphicsConfig& config) {
if (initialized_) return;
window_ = static_cast<SDL_Window*>(window);
enableValidationLayers = config.enableValidationLayers;
CreateInstance(window_);
SetupDebugMessenger();
CreateSurface(window_);
PickPhysicalDevice();
CreateLogicalDevice();
CreateSwapChain();
CreateImageViews();
CreateRenderPass();
CreateFramebuffers();
CreateCommandPool();
CreateCommandBuffers();
CreateSyncObjects();
initialized_ = true;
}
void VulkanGraphicsBackend::Shutdown() {
if (!initialized_) return;
vkDeviceWaitIdle(device_);
CleanupSwapChain();
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
vkDestroySemaphore(device_, renderFinishedSemaphores_[i], nullptr);
vkDestroySemaphore(device_, imageAvailableSemaphores_[i], nullptr);
vkDestroyFence(device_, inFlightFences_[i], nullptr);
}
vkDestroyCommandPool(device_, commandPool_, nullptr);
vkDestroyDevice(device_, nullptr);
if (enableValidationLayers) {
DestroyDebugUtilsMessengerEXT(instance_, debugMessenger_, nullptr);
}
vkDestroySurfaceKHR(instance_, surface_, nullptr);
vkDestroyInstance(instance_, nullptr);
initialized_ = false;
}
GraphicsDeviceHandle VulkanGraphicsBackend::CreateDevice() {
// Return device handle
return static_cast<GraphicsDeviceHandle>(device_);
}
void VulkanGraphicsBackend::DestroyDevice(GraphicsDeviceHandle device) {
// Device is destroyed in Shutdown
}
GraphicsPipelineHandle VulkanGraphicsBackend::CreatePipeline(GraphicsDeviceHandle device, const ShaderPaths& shaderPaths) {
// TODO: Create pipeline
return nullptr;
}
void VulkanGraphicsBackend::DestroyPipeline(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline) {
// TODO: Destroy pipeline
}
GraphicsBufferHandle VulkanGraphicsBackend::CreateVertexBuffer(GraphicsDeviceHandle device, const std::vector<uint8_t>& data) {
// TODO: Create vertex buffer
return nullptr;
}
GraphicsBufferHandle VulkanGraphicsBackend::CreateIndexBuffer(GraphicsDeviceHandle device, const std::vector<uint8_t>& data) {
// TODO: Create index buffer
return nullptr;
}
void VulkanGraphicsBackend::DestroyBuffer(GraphicsDeviceHandle device, GraphicsBufferHandle buffer) {
// TODO: Destroy buffer
}
bool VulkanGraphicsBackend::BeginFrame(GraphicsDeviceHandle device) {
// TODO: Begin frame
return true;
}
bool VulkanGraphicsBackend::EndFrame(GraphicsDeviceHandle device) {
// TODO: End frame
return true;
}
void VulkanGraphicsBackend::Draw(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline,
GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer,
uint32_t indexCount, const std::array<float, 16>& modelMatrix) {
// TODO: Draw
}
void VulkanGraphicsBackend::CreateInstance(SDL_Window* window) {
if (enableValidationLayers && !CheckValidationLayerSupport()) {
throw std::runtime_error("validation layers requested, but not available!");
}
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "SDL3CPlusPlus";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
auto extensions = GetRequiredExtensions(window);
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();
VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
PopulateDebugMessengerCreateInfo(debugCreateInfo);
createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*)&debugCreateInfo;
} else {
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
}
if (vkCreateInstance(&createInfo, nullptr, &instance_) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
}
void VulkanGraphicsBackend::PopulateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = debugCallback;
}
void VulkanGraphicsBackend::SetupDebugMessenger() {
if (!enableValidationLayers) return;
VkDebugUtilsMessengerCreateInfoEXT createInfo;
PopulateDebugMessengerCreateInfo(createInfo);
if (CreateDebugUtilsMessengerEXT(instance_, &createInfo, nullptr, &debugMessenger_) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug messenger!");
}
}
void VulkanGraphicsBackend::CreateSurface(SDL_Window* window) {
if (!SDL_Vulkan_CreateSurface(window, instance_, &surface_)) {
throw std::runtime_error("failed to create window surface!");
}
}
void VulkanGraphicsBackend::PickPhysicalDevice() {
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance_, &deviceCount, nullptr);
if (deviceCount == 0) {
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance_, &deviceCount, devices.data());
for (const auto& device : devices) {
if (IsDeviceSuitable(device)) {
physicalDevice_ = device;
break;
}
}
if (physicalDevice_ == VK_NULL_HANDLE) {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
void VulkanGraphicsBackend::CreateLogicalDevice() {
QueueFamilyIndices indices = FindQueueFamilies(physicalDevice_);
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};
float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFamilies) {
VkDeviceQueueCreateInfo queueCreateInfo{};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = queueFamily;
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &queuePriority;
queueCreateInfos.push_back(queueCreateInfo);
}
VkPhysicalDeviceFeatures deviceFeatures{};
VkDeviceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
if (enableValidationLayers) {
createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
} else {
createInfo.enabledLayerCount = 0;
}
if (vkCreateDevice(physicalDevice_, &createInfo, nullptr, &device_) != VK_SUCCESS) {
throw std::runtime_error("failed to create logical device!");
}
vkGetDeviceQueue(device_, indices.graphicsFamily.value(), 0, &graphicsQueue_);
vkGetDeviceQueue(device_, indices.presentFamily.value(), 0, &presentQueue_);
}
void VulkanGraphicsBackend::CreateSwapChain() {
SwapChainSupportDetails swapChainSupport = QuerySwapChainSupport(physicalDevice_);
VkSurfaceFormatKHR surfaceFormat = ChooseSwapSurfaceFormat(swapChainSupport.formats);
VkPresentModeKHR presentMode = ChooseSwapPresentMode(swapChainSupport.presentModes);
VkExtent2D extent = ChooseSwapExtent(swapChainSupport.capabilities);
uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
imageCount = swapChainSupport.capabilities.maxImageCount;
}
VkSwapchainCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface_;
createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
QueueFamilyIndices indices = FindQueueFamilies(physicalDevice_);
uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()};
if (indices.graphicsFamily != indices.presentFamily) {
createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
createInfo.queueFamilyIndexCount = 2;
createInfo.pQueueFamilyIndices = queueFamilyIndices;
} else {
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
}
createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
if (vkCreateSwapchainKHR(device_, &createInfo, nullptr, &swapchain_) != VK_SUCCESS) {
throw std::runtime_error("failed to create swap chain!");
}
vkGetSwapchainImagesKHR(device_, swapchain_, &imageCount, nullptr);
swapchainImages_.resize(imageCount);
vkGetSwapchainImagesKHR(device_, swapchain_, &imageCount, swapchainImages_.data());
swapchainImageFormat_ = surfaceFormat.format;
swapchainExtent_ = extent;
}
void VulkanGraphicsBackend::CreateImageViews() {
swapchainImageViews_.resize(swapchainImages_.size());
for (size_t i = 0; i < swapchainImages_.size(); i++) {
VkImageViewCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = swapchainImages_[i];
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = swapchainImageFormat_;
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
if (vkCreateImageView(device_, &createInfo, nullptr, &swapchainImageViews_[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create image views!");
}
}
}
void VulkanGraphicsBackend::CreateRenderPass() {
VkAttachmentDescription colorAttachment{};
colorAttachment.format = swapchainImageFormat_;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;
if (vkCreateRenderPass(device_, &renderPassInfo, nullptr, &renderPass_) != VK_SUCCESS) {
throw std::runtime_error("failed to create render pass!");
}
}
void VulkanGraphicsBackend::CreateFramebuffers() {
framebuffers_.resize(swapchainImageViews_.size());
for (size_t i = 0; i < swapchainImageViews_.size(); i++) {
VkImageView attachments[] = {
swapchainImageViews_[i]
};
VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass_;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = attachments;
framebufferInfo.width = swapchainExtent_.width;
framebufferInfo.height = swapchainExtent_.height;
framebufferInfo.layers = 1;
if (vkCreateFramebuffer(device_, &framebufferInfo, nullptr, &framebuffers_[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create framebuffer!");
}
}
}
void VulkanGraphicsBackend::CreateCommandPool() {
QueueFamilyIndices queueFamilyIndices = FindQueueFamilies(physicalDevice_);
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
if (vkCreateCommandPool(device_, &poolInfo, nullptr, &commandPool_) != VK_SUCCESS) {
throw std::runtime_error("failed to create command pool!");
}
}
void VulkanGraphicsBackend::CreateCommandBuffers() {
commandBuffers_.resize(MAX_FRAMES_IN_FLIGHT);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool_;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t) commandBuffers_.size();
if (vkAllocateCommandBuffers(device_, &allocInfo, commandBuffers_.data()) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate command buffers!");
}
}
void VulkanGraphicsBackend::CreateSyncObjects() {
imageAvailableSemaphores_.resize(MAX_FRAMES_IN_FLIGHT);
renderFinishedSemaphores_.resize(MAX_FRAMES_IN_FLIGHT);
inFlightFences_.resize(MAX_FRAMES_IN_FLIGHT);
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (vkCreateSemaphore(device_, &semaphoreInfo, nullptr, &imageAvailableSemaphores_[i]) != VK_SUCCESS ||
vkCreateSemaphore(device_, &semaphoreInfo, nullptr, &renderFinishedSemaphores_[i]) != VK_SUCCESS ||
vkCreateFence(device_, &fenceInfo, nullptr, &inFlightFences_[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create synchronization objects for a frame!");
}
}
}
void VulkanGraphicsBackend::CleanupSwapChain() {
for (auto framebuffer : framebuffers_) {
vkDestroyFramebuffer(device_, framebuffer, nullptr);
}
for (auto imageView : swapchainImageViews_) {
vkDestroyImageView(device_, imageView, nullptr);
}
vkDestroySwapchainKHR(device_, swapchain_, nullptr);
}
std::vector<const char*> VulkanGraphicsBackend::GetRequiredExtensions(SDL_Window* window) {
uint32_t sdlExtensionCount = 0;
const char** sdlExtensions = SDL_Vulkan_GetInstanceExtensions(&sdlExtensionCount);
std::vector<const char*> extensions(sdlExtensions, sdlExtensions + sdlExtensionCount);
if (enableValidationLayers) {
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
return extensions;
}
bool VulkanGraphicsBackend::CheckValidationLayerSupport() {
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
for (const char* layerName : validationLayers) {
bool layerFound = false;
for (const auto& layerProperties : availableLayers) {
if (strcmp(layerName, layerProperties.layerName) == 0) {
layerFound = true;
break;
}
}
if (!layerFound) {
return false;
}
}
return true;
}
bool VulkanGraphicsBackend::IsDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = FindQueueFamilies(device);
bool extensionsSupported = CheckDeviceExtensionSupport(device);
bool swapChainAdequate = false;
if (extensionsSupported) {
SwapChainSupportDetails swapChainSupport = QuerySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}
return indices.isComplete() && extensionsSupported && swapChainAdequate;
}
bool VulkanGraphicsBackend::CheckDeviceExtensionSupport(VkPhysicalDevice device) {
uint32_t extensionCount;
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());
std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());
for (const auto& extension : availableExtensions) {
requiredExtensions.erase(extension.extensionName);
}
return requiredExtensions.empty();
}
QueueFamilyIndices VulkanGraphicsBackend::FindQueueFamilies(VkPhysicalDevice device) {
QueueFamilyIndices indices;
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());
int i = 0;
for (const auto& queueFamily : queueFamilies) {
if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
indices.graphicsFamily = i;
}
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface_, &presentSupport);
if (presentSupport) {
indices.presentFamily = i;
}
if (indices.isComplete()) {
break;
}
i++;
}
return indices;
}
SwapChainSupportDetails VulkanGraphicsBackend::QuerySwapChainSupport(VkPhysicalDevice device) {
SwapChainSupportDetails details;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface_, &details.capabilities);
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface_, &formatCount, nullptr);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface_, &formatCount, details.formats.data());
}
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface_, &presentModeCount, nullptr);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface_, &presentModeCount, details.presentModes.data());
}
return details;
}
VkSurfaceFormatKHR VulkanGraphicsBackend::ChooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
for (const auto& availableFormat : availableFormats) {
if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
return availableFormat;
}
}
return availableFormats[0];
}
VkPresentModeKHR VulkanGraphicsBackend::ChooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
for (const auto& availablePresentMode : availablePresentModes) {
if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
return availablePresentMode;
}
}
return VK_PRESENT_MODE_FIFO_KHR;
}
VkExtent2D VulkanGraphicsBackend::ChooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities, SDL_Window* window) {
if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
return capabilities.currentExtent;
} else {
int width, height;
SDL_Vulkan_GetDrawableSize(window, &width, &height);
VkExtent2D actualExtent = {
static_cast<uint32_t>(width),
static_cast<uint32_t>(height)
};
actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height);
return actualExtent;
}
}
} // namespace sdl3cpp::services::impl

View File

@@ -74,14 +74,15 @@ void VulkanGuiService::RenderToSwapchain(VkCommandBuffer commandBuffer, VkImage
rendererService_->RenderToSwapchain(commandBuffer, image);
}
void VulkanGuiService::Resize(uint32_t width, uint32_t height, VkFormat format) {
void VulkanGuiService::Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass) {
logger_->Trace("VulkanGuiService", "Resize",
"width=" + std::to_string(width) +
", height=" + std::to_string(height) +
", format=" + std::to_string(static_cast<uint32_t>(format)));
", format=" + std::to_string(static_cast<uint32_t>(format)) +
", renderPassIsNull=" + std::string(renderPass == VK_NULL_HANDLE ? "true" : "false"));
if (rendererService_) {
rendererService_->Resize(width, height, format);
rendererService_->Resize(width, height, format, renderPass);
}
}

View File

@@ -34,7 +34,7 @@ public:
void RenderToSwapchain(VkCommandBuffer commandBuffer, VkImage image) override;
void Resize(uint32_t width, uint32_t height, VkFormat format) override;
void Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass) override;
void Shutdown() noexcept override;

View File

@@ -4,6 +4,7 @@
#include <string>
#include <vector>
#include <array>
#include <utility>
#include "graphics_types.hpp"
namespace sdl3cpp::services {
@@ -51,6 +52,19 @@ public:
*/
virtual void Shutdown() = 0;
/**
* @brief Recreate the swapchain for a new window size.
*
* @param width New width in pixels
* @param height New height in pixels
*/
virtual void RecreateSwapchain(uint32_t width, uint32_t height) = 0;
/**
* @brief Wait for GPU operations to complete.
*/
virtual void WaitIdle() = 0;
/**
* @brief Create a graphics device.
*
@@ -145,6 +159,41 @@ public:
virtual void Draw(GraphicsDeviceHandle device, GraphicsPipelineHandle pipeline,
GraphicsBufferHandle vertexBuffer, GraphicsBufferHandle indexBuffer,
uint32_t indexCount, const std::array<float, 16>& modelMatrix) = 0;
/**
* @brief Get the physical device handle.
*
* @return Opaque physical device handle
*/
virtual GraphicsDeviceHandle GetPhysicalDevice() const = 0;
/**
* @brief Get the swapchain extent.
*
* @return Width and height in pixels
*/
virtual std::pair<uint32_t, uint32_t> GetSwapchainExtent() const = 0;
/**
* @brief Get the swapchain image format.
*
* @return Format identifier
*/
virtual uint32_t GetSwapchainFormat() const = 0;
/**
* @brief Get the current command buffer.
*
* @return Opaque command buffer handle
*/
virtual void* GetCurrentCommandBuffer() const = 0;
/**
* @brief Get the graphics queue handle.
*
* @return Opaque queue handle
*/
virtual void* GetGraphicsQueue() const = 0;
};
} // namespace sdl3cpp::services
} // namespace sdl3cpp::services

View File

@@ -23,7 +23,7 @@ public:
virtual void RenderToSwapchain(VkCommandBuffer commandBuffer, VkImage image) = 0;
virtual void Resize(uint32_t width, uint32_t height, VkFormat format) = 0;
virtual void Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass) = 0;
virtual void Shutdown() noexcept = 0;

View File

@@ -68,7 +68,7 @@ public:
* @param height New height in pixels
* @param format Swapchain image format
*/
virtual void Resize(uint32_t width, uint32_t height, VkFormat format) = 0;
virtual void Resize(uint32_t width, uint32_t height, VkFormat format, VkRenderPass renderPass) = 0;
/**
* @brief Shutdown and release GPU resources.

View File

@@ -75,6 +75,13 @@ public:
* @return Max concurrent frames
*/
virtual uint32_t GetMaxFramesInFlight() const = 0;
/**
* @brief Handle swapchain recreation.
*
* Resets command buffers/synchronization and updates GUI renderer state.
*/
virtual void OnSwapchainRecreated() = 0;
};
} // namespace sdl3cpp::services