#include "stb_truetype.h" #include "stb_image.h" #include "vui_internal.h" namespace vui { Renderer::Renderer() : impl(new Impl) {} Renderer::~Renderer() { delete impl; } void Renderer::init(VkDevice device, VkPhysicalDevice physDevice, VkQueue graphicsQueue, uint32_t queueFamily, VkFormat swapchainFormat, uint32_t imageCount) { impl->device = device; impl->physDevice = physDevice; impl->graphicsQueue = graphicsQueue; impl->queueFamily = queueFamily; impl->swapchainFormat = swapchainFormat; impl->imageCount = imageCount; impl->initRenderPass(); impl->initSampler(); impl->initDescriptorPool(); impl->initPipeline(); impl->initVertexBuffer(INITIAL_VERTEX_COUNT); impl->createWhiteTexture(); impl->currentTransform = Mat3::identity(); memset(&impl->input, 0, sizeof(impl->input)); } void Renderer::shutdown() { vkDeviceWaitIdle(impl->device); for (auto &tex : impl->textures) { if (!tex.active) continue; vkDestroyImageView(impl->device, tex.view, nullptr); vkDestroyImage(impl->device, tex.image, nullptr); vkFreeMemory(impl->device, tex.memory, nullptr); } impl->textures.clear(); impl->fonts.clear(); if (impl->vertexBuffer) { vkDestroyBuffer(impl->device, impl->vertexBuffer, nullptr); vkFreeMemory(impl->device, impl->vertexMemory, nullptr); } for (auto fb : impl->overlayFramebuffers) vkDestroyFramebuffer(impl->device, fb, nullptr); impl->overlayFramebuffers.clear(); if (impl->pipeline) vkDestroyPipeline(impl->device, impl->pipeline, nullptr); if (impl->pipelineLayout) vkDestroyPipelineLayout(impl->device, impl->pipelineLayout, nullptr); if (impl->descriptorPool) vkDestroyDescriptorPool(impl->device, impl->descriptorPool, nullptr); if (impl->descriptorSetLayout) vkDestroyDescriptorSetLayout(impl->device, impl->descriptorSetLayout, nullptr); if (impl->sampler) vkDestroySampler(impl->device, impl->sampler, nullptr); if (impl->renderPass) vkDestroyRenderPass(impl->device, impl->renderPass, nullptr); } void Renderer::setOverlayFramebuffers(VkImageView *views, uint32_t count, uint32_t width, uint32_t height) { for (auto fb : impl->overlayFramebuffers) vkDestroyFramebuffer(impl->device, fb, nullptr); impl->overlayFramebuffers.clear(); impl->overlayWidth = width; impl->overlayHeight = height; impl->overlayFramebuffers.resize(count); for (uint32_t i = 0; i < count; i++) { VkFramebufferCreateInfo fci{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; fci.renderPass = impl->renderPass; fci.attachmentCount = 1; fci.pAttachments = &views[i]; fci.width = width; fci.height = height; fci.layers = 1; vkCreateFramebuffer(impl->device, &fci, nullptr, &impl->overlayFramebuffers[i]); } } VkRenderPass Renderer::getRenderPass() const { return impl->renderPass; } void Renderer::beginFrame(int windowW, int windowH, int framebufferW, int framebufferH) { impl->winW = windowW; impl->winH = windowH; impl->fbW = (framebufferW > 0) ? framebufferW : windowW; impl->fbH = (framebufferH > 0) ? framebufferH : windowH; impl->vertices.clear(); impl->drawCmds.clear(); impl->transformStack.clear(); impl->scissorStack.clear(); impl->currentTransform = Mat3::identity(); impl->frameActive = true; memcpy(impl->input.prevMouseButtons, impl->input.mouseButtons, sizeof(impl->input.mouseButtons)); memcpy(impl->input.prevKeys, impl->input.keys, sizeof(impl->input.keys)); memset(impl->input.keyClicked, 0, sizeof(impl->input.keyClicked)); memset(impl->input.mouseClicked, 0, sizeof(impl->input.mouseClicked)); impl->input.scrollX = 0; impl->input.scrollY = 0; impl->input.charCount = 0; } void Renderer::endFrame(VkCommandBuffer cmd, uint32_t imageIndex) { impl->frameActive = false; if (impl->vertices.empty()) return; if (impl->vertices.size() > impl->vertexBufferCapacity) { uint32_t newCap = (uint32_t)impl->vertices.size() * 2; impl->initVertexBuffer(newCap); } void *mapped; vkMapMemory(impl->device, impl->vertexMemory, 0, sizeof(Vertex) * impl->vertices.size(), 0, &mapped); memcpy(mapped, impl->vertices.data(), sizeof(Vertex) * impl->vertices.size()); vkUnmapMemory(impl->device, impl->vertexMemory); bool ownPass = !impl->overlayFramebuffers.empty() && imageIndex < (uint32_t)impl->overlayFramebuffers.size(); if (ownPass) { VkRenderPassBeginInfo rpbi{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; rpbi.renderPass = impl->renderPass; rpbi.framebuffer = impl->overlayFramebuffers[imageIndex]; rpbi.renderArea.extent = {impl->overlayWidth, impl->overlayHeight}; vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); } float L = 0, R = (float)impl->winW, T = 0, B = (float)impl->winH; float projection[16] = { 2.0f/(R-L), 0.0f, 0.0f, 0.0f, 0.0f, 2.0f/(B-T), 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, -(R+L)/(R-L), -(B+T)/(B-T), 0.0f, 1.0f, }; vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, impl->pipeline); vkCmdPushConstants(cmd, impl->pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(projection), projection); VkViewport viewport{0, 0, (float)impl->fbW, (float)impl->fbH, 0.0f, 1.0f}; vkCmdSetViewport(cmd, 0, 1, &viewport); VkRect2D fullScissor{{0,0}, {(uint32_t)impl->fbW, (uint32_t)impl->fbH}}; vkCmdSetScissor(cmd, 0, 1, &fullScissor); VkDeviceSize offset = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &impl->vertexBuffer, &offset); float scaleX = (float)impl->fbW / (float)impl->winW; float scaleY = (float)impl->fbH / (float)impl->winH; int lastTexId = -1; for (auto &dc : impl->drawCmds) { if (dc.textureId != lastTexId) { if (dc.textureId >= 0 && dc.textureId < (int)impl->textures.size()) { vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, impl->pipelineLayout, 0, 1, &impl->textures[dc.textureId].descriptorSet, 0, nullptr); } lastTexId = dc.textureId; } if (dc.hasScissor) { VkRect2D sc; sc.offset.x = std::max(0, (int)(dc.scissorX * scaleX)); sc.offset.y = std::max(0, (int)(dc.scissorY * scaleY)); sc.extent.width = (uint32_t)std::max(0.0f, dc.scissorW * scaleX); sc.extent.height = (uint32_t)std::max(0.0f, dc.scissorH * scaleY); vkCmdSetScissor(cmd, 0, 1, &sc); } else { vkCmdSetScissor(cmd, 0, 1, &fullScissor); } vkCmdDraw(cmd, dc.vertexCount, 1, dc.vertexOffset, 0); } if (ownPass) { vkCmdEndRenderPass(cmd); } } void Renderer::drawRect(float x, float y, float w, float h, Color color) { impl->addQuad(WHITE_TEXTURE_ID, x, y, x+w, y+h, 0, 0, 1, 1, color); } void Renderer::drawRectOutline(float x, float y, float w, float h, float t, Color color) { drawRect(x, y, w, t, color); drawRect(x, y+h-t, w, t, color); drawRect(x, y+t, t, h-2*t, color); drawRect(x+w-t, y+t, t, h-2*t, color); } void Renderer::drawImage(int texId, float x, float y, float w, float h, Color tint) { impl->addQuad(texId, x, y, x+w, y+h, 0, 0, 1, 1, tint); } void Renderer::drawImageRegion(int texId, float x, float y, float w, float h, float u0, float v0, float u1, float v1, Color tint) { impl->addQuad(texId, x, y, x+w, y+h, u0, v0, u1, v1, tint); } void Renderer::drawImage9Slice(int texId, float x, float y, float w, float h, float bL, float bR, float bT, float bB, int texW, int texH, Color tint) { float uL = bL / texW, uR = 1.0f - bR / texW; float vT = bT / texH, vB = 1.0f - bB / texH; float midW = w - bL - bR, midH = h - bT - bB; drawImageRegion(texId, x, y, bL, bT, 0, 0, uL, vT, tint); drawImageRegion(texId, x+bL, y, midW, bT, uL, 0, uR, vT, tint); drawImageRegion(texId, x+bL+midW, y, bR, bT, uR, 0, 1, vT, tint); drawImageRegion(texId, x, y+bT, bL, midH, 0, vT, uL, vB, tint); drawImageRegion(texId, x+bL, y+bT, midW, midH, uL, vT, uR, vB, tint); drawImageRegion(texId, x+bL+midW, y+bT, bR, midH, uR, vT, 1, vB, tint); drawImageRegion(texId, x, y+bT+midH, bL, bB, 0, vB, uL, 1, tint); drawImageRegion(texId, x+bL, y+bT+midH, midW, bB, uL, vB, uR, 1, tint); drawImageRegion(texId, x+bL+midW, y+bT+midH, bR, bB, uR, vB, 1, 1, tint); } void Renderer::drawText(const char *text, float x, float y, Color color, int fontId) { if (fontId < 0 || fontId >= (int)impl->fonts.size() || !impl->fonts[fontId].active) return; auto &font = impl->fonts[fontId]; float cx = x; float cy = y + font.ascent; for (const char *p = text; *p; p++) { int ch = (unsigned char)*p; if (ch >= 128) continue; auto &g = font.glyphs[ch]; if (g.x1 > g.x0) { float gx = cx + g.xOff; float gy = cy + g.yOff; impl->addQuad(font.textureId, gx, gy, gx + (g.x1 - g.x0), gy + (g.y1 - g.y0), g.u0, g.v0, g.u1, g.v1, color); } cx += g.xAdvance; } } void Renderer::drawTextCentered(const char *text, float x, float y, float w, float h, Color color, int fontId) { float tw = measureText(text, fontId); float th = fontHeight(fontId); float tx = x + (w - tw) * 0.5f; float ty = y + (h - th) * 0.5f; drawText(text, tx, ty, color, fontId); } void Renderer::drawTextWrapped(const char *text, float x, float y, float maxWidth, Color color, int fontId) { if (fontId < 0 || fontId >= (int)impl->fonts.size() || !impl->fonts[fontId].active) return; auto &font = impl->fonts[fontId]; float cx = x; float cy = y; float lineH = font.ascent - font.descent + font.lineGap; const char *wordStart = text; while (*wordStart) { while (*wordStart == ' ') { int ch = (unsigned char)*wordStart; if (ch < 128) cx += font.glyphs[ch].xAdvance; wordStart++; } if (!*wordStart) break; const char *wordEnd = wordStart; float wordW = 0; while (*wordEnd && *wordEnd != ' ') { int ch = (unsigned char)*wordEnd; if (ch < 128) wordW += font.glyphs[ch].xAdvance; wordEnd++; } if (cx + wordW > x + maxWidth && cx > x) { cx = x; cy += lineH; } for (const char *p = wordStart; p < wordEnd; p++) { int ch = (unsigned char)*p; if (ch < 128) { auto &g = font.glyphs[ch]; if (g.x1 > g.x0) { float gx = cx + g.xOff; float gy = cy + font.ascent + g.yOff; impl->addQuad(font.textureId, gx, gy, gx + (g.x1 - g.x0), gy + (g.y1 - g.y0), g.u0, g.v0, g.u1, g.v1, color); } cx += g.xAdvance; } } wordStart = wordEnd; } } float Renderer::measureText(const char *text, int fontId) { if (fontId < 0 || fontId >= (int)impl->fonts.size() || !impl->fonts[fontId].active) return 0; auto &font = impl->fonts[fontId]; float w = 0; for (const char *p = text; *p; p++) { int ch = (unsigned char)*p; if (ch < 128) w += font.glyphs[ch].xAdvance; } return w; } float Renderer::fontHeight(int fontId) { if (fontId < 0 || fontId >= (int)impl->fonts.size() || !impl->fonts[fontId].active) return 0; auto &f = impl->fonts[fontId]; return f.ascent - f.descent; } int Renderer::createTexture(int width, int height, const void *rgba) { return impl->allocTexture(width, height, rgba); } void Renderer::updateTexture(int id, int x, int y, int w, int h, const void *rgba) { if (id < 0 || id >= (int)impl->textures.size() || !impl->textures[id].active) return; impl->uploadTextureData(impl->textures[id], x, y, w, h, rgba); } void Renderer::destroyTexture(int id) { if (id < 0 || id >= (int)impl->textures.size() || !impl->textures[id].active) return; auto &tex = impl->textures[id]; vkDeviceWaitIdle(impl->device); vkDestroyImageView(impl->device, tex.view, nullptr); vkDestroyImage(impl->device, tex.image, nullptr); vkFreeMemory(impl->device, tex.memory, nullptr); tex.active = false; } int Renderer::loadTexture(const char *path) { int w, h, channels; unsigned char *data = stbi_load(path, &w, &h, &channels, 4); if (!data) { fprintf(stderr, "vui: failed to load texture '%s'\n", path); return -1; } int id = createTexture(w, h, data); stbi_image_free(data); return id; } int Renderer::loadFont(const char *path, float size) { FILE *f = fopen(path, "rb"); if (!f) { fprintf(stderr, "vui: failed to open font '%s'\n", path); return -1; } fseek(f, 0, SEEK_END); long len = ftell(f); fseek(f, 0, SEEK_SET); std::vector buf(len); fread(buf.data(), 1, len, f); fclose(f); return loadFontFromMemory(buf.data(), (int)len, size); } int Renderer::loadFontFromMemory(const void *data, int dataSize, float size) { (void)dataSize; stbtt_fontinfo info; if (!stbtt_InitFont(&info, (const unsigned char *)data, 0)) { fprintf(stderr, "vui: failed to init font\n"); return -1; } float fontScale = stbtt_ScaleForPixelHeight(&info, size); int ascent, descent, lineGap; stbtt_GetFontVMetrics(&info, &ascent, &descent, &lineGap); int atlasW = 512, atlasH = 512; std::vector bitmap(atlasW * atlasH, 0); stbtt_bakedchar cdata[128]; stbtt_BakeFontBitmap((const unsigned char *)data, 0, size, bitmap.data(), atlasW, atlasH, 0, 128, cdata); std::vector rgba(atlasW * atlasH); for (int i = 0; i < atlasW * atlasH; i++) { uint8_t a = bitmap[i]; rgba[i] = (uint32_t)a << 24 | 0x00FFFFFF; } int texId = createTexture(atlasW, atlasH, rgba.data()); FontEntry fe{}; fe.textureId = texId; fe.size = size; fe.ascent = ascent * fontScale; fe.descent = descent * fontScale; fe.lineGap = lineGap * fontScale; fe.active = true; for (int i = 0; i < 128; i++) { auto &bc = cdata[i]; fe.glyphs[i].x0 = (float)bc.x0; fe.glyphs[i].y0 = (float)bc.y0; fe.glyphs[i].x1 = (float)bc.x1; fe.glyphs[i].y1 = (float)bc.y1; fe.glyphs[i].u0 = (float)bc.x0 / atlasW; fe.glyphs[i].v0 = (float)bc.y0 / atlasH; fe.glyphs[i].u1 = (float)bc.x1 / atlasW; fe.glyphs[i].v1 = (float)bc.y1 / atlasH; fe.glyphs[i].xAdvance = bc.xadvance; fe.glyphs[i].xOff = bc.xoff; fe.glyphs[i].yOff = bc.yoff; } int id = (int)impl->fonts.size(); impl->fonts.push_back(fe); return id; } void Renderer::pushScissor(float x, float y, float w, float h) { impl->scissorStack.push_back({x, y, w, h}); } void Renderer::popScissor() { if (!impl->scissorStack.empty()) impl->scissorStack.pop_back(); } void Renderer::pushTransform() { impl->transformStack.push_back(impl->currentTransform); } void Renderer::popTransform() { if (!impl->transformStack.empty()) { impl->currentTransform = impl->transformStack.back(); impl->transformStack.pop_back(); } } void Renderer::translate(float x, float y) { Mat3 t; t.m[2] = x; t.m[5] = y; impl->currentTransform = impl->currentTransform * t; } void Renderer::rotate(float angleDeg) { float rad = angleDeg * 3.14159265f / 180.0f; float c = cosf(rad), s = sinf(rad); Mat3 r; r.m[0] = c; r.m[1] = -s; r.m[3] = s; r.m[4] = c; impl->currentTransform = impl->currentTransform * r; } void Renderer::scale(float sx, float sy) { Mat3 s; s.m[0] = sx; s.m[4] = sy; impl->currentTransform = impl->currentTransform * s; } void Renderer::feedMousePosition(float x, float y) { impl->input.mouseX = x; impl->input.mouseY = y; } void Renderer::feedMouseButton(int button, bool pressed) { if (button >= 0 && button < 5) { if (pressed && !impl->input.mouseButtons[button]) impl->input.mouseClicked[button] = true; impl->input.mouseButtons[button] = pressed; } } void Renderer::feedMouseScroll(float dx, float dy) { impl->input.scrollX += dx; impl->input.scrollY += dy; } void Renderer::feedKey(int key, bool pressed) { if (key >= 0 && key < 512) { if (pressed && !impl->input.keys[key]) impl->input.keyClicked[key] = true; impl->input.keys[key] = pressed; } } void Renderer::feedChar(unsigned int codepoint) { if (impl->input.charCount < 32) impl->input.charBuffer[impl->input.charCount++] = codepoint; } bool Renderer::isMouseOver(float x, float y, float w, float h) { return impl->input.mouseX >= x && impl->input.mouseX < x + w && impl->input.mouseY >= y && impl->input.mouseY < y + h; } bool Renderer::isMouseClicked(float x, float y, float w, float h) { return isMouseOver(x, y, w, h) && impl->input.mouseClicked[0]; } bool Renderer::isMouseDown(float x, float y, float w, float h) { return isMouseOver(x, y, w, h) && impl->input.mouseButtons[0]; } bool Renderer::isKeyPressed(int key) { if (key < 0 || key >= 512) return false; return impl->input.keys[key]; } bool Renderer::isKeyClicked(int key) { if (key < 0 || key >= 512) return false; return impl->input.keyClicked[key]; } bool Renderer::isMouseDownAnywhere() { return impl->input.mouseButtons[0]; } bool Renderer::isMouseClickedAnywhere() { return impl->input.mouseClicked[0]; } float Renderer::mouseX() { return impl->input.mouseX; } float Renderer::mouseY() { return impl->input.mouseY; } float Renderer::scrollDeltaX() { return impl->input.scrollX; } float Renderer::scrollDeltaY() { return impl->input.scrollY; } int Renderer::pollChar() { if (impl->input.charCount <= 0) return -1; unsigned int c = impl->input.charBuffer[0]; for (int i = 1; i < impl->input.charCount; i++) impl->input.charBuffer[i - 1] = impl->input.charBuffer[i]; impl->input.charCount--; return static_cast(c); } int Renderer::windowW() const { return impl->winW; } int Renderer::windowH() const { return impl->winH; } int Renderer::textureWidth(int id) const { if (id < 0 || id >= (int)impl->textures.size() || !impl->textures[id].active) return 0; return impl->textures[id].width; } int Renderer::textureHeight(int id) const { if (id < 0 || id >= (int)impl->textures.size() || !impl->textures[id].active) return 0; return impl->textures[id].height; } } // namespace vui