#include #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX #define NOMINMAX #endif #include #elif defined(__APPLE__) #include #else #include #endif #include "main.h" #include "gl_renderer.h" #include #include #include #include #include #include #ifdef _WIN32 static void* rmeGetGLProc(const char* name) { auto p = (void*)wglGetProcAddress(name); if (p == nullptr || p == (void*)0x1 || p == (void*)0x2 || p == (void*)0x3 || p == (void*)-1) { static HMODULE gl = LoadLibraryA("opengl32.dll"); p = (void*)GetProcAddress(gl, name); } return p; } #elif defined(__APPLE__) static void* rmeGetGLProc(const char* name) { static void* lib = dlopen("/System/Library/Frameworks/OpenGL.framework/OpenGL", RTLD_LAZY); return lib ? dlsym(lib, name) : nullptr; } #else typedef void (*__GLXextFuncPtr)(void); extern "C" __GLXextFuncPtr glXGetProcAddressARB(const unsigned char*); static void* rmeGetGLProc(const char* name) { void* p = (void*)glXGetProcAddressARB((const GLubyte*)name); if (!p) { static void* lib = dlopen("libGL.so.1", RTLD_LAZY); if (lib) { p = dlsym(lib, name); } } return p; } #endif std::vector GLRenderer::s_instances; static const char* const vertSrc = R"( #version 330 layout(location=0) in vec2 aPos; layout(location=1) in vec2 aUV; layout(location=2) in vec4 aColor; uniform mat4 uProjection; out vec2 vUV; out vec4 vColor; void main(){ gl_Position = uProjection * vec4(aPos, 0.0, 1.0); vUV = aUV; vColor = aColor; } )"; static const char* const fragSrc = R"( #version 330 in vec2 vUV; in vec4 vColor; uniform sampler2D uTexture; uniform int uStipple; out vec4 FragColor; void main() { if (uStipple != 0) { float p = gl_FragCoord.x + gl_FragCoord.y; if (mod(p, 4.0) < 2.0) discard; } FragColor = texture(uTexture, vUV) * vColor; } )"; void GLRenderer::initFontAtlas() { // Load TTF font const float fontSize = 14.0f; std::string fontPath; #ifdef _WIN32 fontPath = "C:\\Windows\\Fonts\\segoeui.ttf"; #elif defined(__APPLE__) fontPath = "/System/Library/Fonts/Helvetica.ttc"; #else fontPath = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"; #endif // Try bundled font first std::string bundledPath = "data/fonts/DejaVuSans.ttf"; std::ifstream testFile(bundledPath, std::ios::binary); if (testFile.good()) { fontPath = bundledPath; } testFile.close(); std::ifstream file(fontPath, std::ios::binary | std::ios::ate); if (!file.is_open()) { // Fallback to old wxBitmap method initFontAtlasFallback(); return; } auto fileSize = file.tellg(); file.seekg(0); std::vector ttfData(fileSize); file.read(reinterpret_cast(ttfData.data()), fileSize); file.close(); stbtt_fontinfo stbFont; if (!stbtt_InitFont(&stbFont, ttfData.data(), 0)) { initFontAtlasFallback(); return; } float scale = stbtt_ScaleForPixelHeight(&stbFont, fontSize); int ascent; int descent; int lineGap; stbtt_GetFontVMetrics(&stbFont, &ascent, &descent, &lineGap); font.ascent = ascent * scale; font.lineHeight = (ascent - descent + lineGap) * scale; // Bake glyphs into atlas const int texW = 512; const int texH = 512; std::vector bitmap(texW * texH, 0); int penX = 1; int penY = 1; int rowH = 0; for (int i = 0; i < 96; i++) { const auto ch = 32 + i; int x0; int y0; int x1; int y1; stbtt_GetCodepointBitmapBox(&stbFont, ch, scale, scale, &x0, &y0, &x1, &y1); int gw = x1 - x0; int gh = y1 - y0; if (penX + gw + 1 >= texW) { penX = 1; penY += rowH + 1; rowH = 0; } if (penY + gh + 1 >= texH) { break; // atlas full } stbtt_MakeCodepointBitmap(&stbFont, &bitmap[penY * texW + penX], gw, gh, texW, scale, scale, ch); font.glyphs[i].u0 = static_cast(penX) / texW; font.glyphs[i].v0 = static_cast(penY) / texH; font.glyphs[i].u1 = static_cast(penX + gw) / texW; font.glyphs[i].v1 = static_cast(penY + gh) / texH; font.glyphs[i].xoff = static_cast(x0); font.glyphs[i].yoff = static_cast(y0); font.glyphs[i].w = static_cast(gw); font.glyphs[i].h = static_cast(gh); int advW; int lsb; stbtt_GetCodepointHMetrics(&stbFont, ch, &advW, &lsb); font.glyphs[i].advance = advW * scale; font.advances[i] = advW * scale; penX += gw + 1; if (gh + 1 > rowH) { rowH = gh + 1; } } // Upload as RGBA (white + alpha) std::vector pixels(texW * texH * 4); for (int i = 0; i < texW * texH; i++) { pixels[i * 4] = 255; pixels[i * 4 + 1] = 255; pixels[i * 4 + 2] = 255; pixels[i * 4 + 3] = bitmap[i]; } glGenTextures(1, &font.texture); glBindTexture(GL_TEXTURE_2D, font.texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texW, texH, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); glBindTexture(GL_TEXTURE_2D, 0); font.texW = texW; font.texH = texH; font.loaded = true; } void GLRenderer::initFontAtlasFallback() { const int glyphW = 10; const int glyphH = 16; const int cols = 16; const int rows = 6; const int texW = cols * glyphW; const int texH = rows * glyphH; wxBitmap bmp(texW, texH, 24); wxMemoryDC dc(bmp); dc.SetBackground(*wxBLACK_BRUSH); dc.Clear(); dc.SetFont(wxFont(10, wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL)); dc.SetTextForeground(*wxWHITE); for (int i = 0; i < 96; i++) { int col = i % cols; int row = i / cols; const auto ch = static_cast(32 + i); dc.DrawText(wxString(ch), col * glyphW, row * glyphH); } dc.SelectObject(wxNullBitmap); wxImage img = bmp.ConvertToImage(); std::vector pixels(texW * texH * 4); for (int y = 0; y < texH; y++) { for (int x = 0; x < texW; x++) { int si = (y * texW + x) * 3; int di = (y * texW + x) * 4; uint8_t lum = img.GetData()[si]; pixels[di] = 255; pixels[di + 1] = 255; pixels[di + 2] = 255; pixels[di + 3] = lum; } } glGenTextures(1, &font.texture); glBindTexture(GL_TEXTURE_2D, font.texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texW, texH, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); glBindTexture(GL_TEXTURE_2D, 0); font.ascent = 12.0f; font.lineHeight = 16.0f; for (int i = 0; i < 96; i++) { int col = i % cols; int row = i / cols; font.glyphs[i].u0 = static_cast(col * glyphW) / texW; font.glyphs[i].v0 = static_cast(row * glyphH) / texH; font.glyphs[i].u1 = static_cast((col + 1) * glyphW) / texW; font.glyphs[i].v1 = static_cast((row + 1) * glyphH) / texH; font.glyphs[i].xoff = 0; font.glyphs[i].yoff = -12.0f; font.glyphs[i].w = static_cast(glyphW); font.glyphs[i].h = static_cast(glyphH); font.glyphs[i].advance = static_cast(glyphW); font.advances[i] = static_cast(glyphW); } font.texW = texW; font.texH = texH; font.loaded = true; } void GLRenderer::init() { if (std::find(s_instances.begin(), s_instances.end(), this) == s_instances.end()) { s_instances.push_back(this); } if (initialized) { return; } if (!gladLoadGLLoader((GLADloadproc)rmeGetGLProc)) { wxLogError("GLRenderer::init — gladLoadGLLoader failed"); return; } GLuint vs = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vs, 1, &vertSrc, nullptr); glCompileShader(vs); { GLint ok = 0; glGetShaderiv(vs, GL_COMPILE_STATUS, &ok); if (!ok) { std::array log {}; glGetShaderInfoLog(vs, log.size(), nullptr, log.data()); wxLogError("GLRenderer::init — vertex shader compile error: %s", log.data()); glDeleteShader(vs); return; } } GLuint fs = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fs, 1, &fragSrc, nullptr); glCompileShader(fs); { GLint ok = 0; glGetShaderiv(fs, GL_COMPILE_STATUS, &ok); if (!ok) { std::array log {}; glGetShaderInfoLog(fs, log.size(), nullptr, log.data()); wxLogError("GLRenderer::init — fragment shader compile error: %s", log.data()); glDeleteShader(vs); glDeleteShader(fs); return; } } program = glCreateProgram(); glAttachShader(program, vs); glAttachShader(program, fs); glLinkProgram(program); { GLint ok = 0; glGetProgramiv(program, GL_LINK_STATUS, &ok); if (!ok) { std::array log {}; glGetProgramInfoLog(program, log.size(), nullptr, log.data()); wxLogError("GLRenderer::init — program link error: %s", log.data()); glDeleteProgram(program); program = 0; glDeleteShader(vs); glDeleteShader(fs); return; } } glDeleteShader(vs); glDeleteShader(fs); loc_projection = glGetUniformLocation(program, "uProjection"); loc_texture = glGetUniformLocation(program, "uTexture"); loc_stipple = glGetUniformLocation(program, "uStipple"); glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glGenBuffers(1, &ebo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, STREAM_VBO_CAPACITY * sizeof(Vertex), nullptr, GL_DYNAMIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, STREAM_EBO_CAPACITY * sizeof(GLuint), nullptr, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, x)); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, u)); glEnableVertexAttribArray(2); glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex), (void*)offsetof(Vertex, r)); glBindVertexArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); initFontAtlas(); std::array white = { 255, 255, 255, 255 }; glGenTextures(1, &whitePixelTexture); glBindTexture(GL_TEXTURE_2D, whitePixelTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white.data()); glBindTexture(GL_TEXTURE_2D, 0); initialized = true; } void GLRenderer::shutdown() { current_texture = 0; std::erase(s_instances, this); if (!initialized) { return; } destroyFBO(); if (program) { glDeleteProgram(program); program = 0; } if (vbo) { glDeleteBuffers(1, &vbo); vbo = 0; } if (ebo) { glDeleteBuffers(1, &ebo); ebo = 0; } if (vao) { glDeleteVertexArrays(1, &vao); vao = 0; } if (whitePixelTexture) { glDeleteTextures(1, &whitePixelTexture); whitePixelTexture = 0; } if (font.texture) { glDeleteTextures(1, &font.texture); font.texture = 0; } initialized = false; } void GLRenderer::setOrtho(float left, float right, float bottom, float top) { std::array m {}; m[0] = 2.0f / (right - left); m[5] = 2.0f / (top - bottom); m[10] = -1.0f; m[12] = -(right + left) / (right - left); m[13] = -(top + bottom) / (top - bottom); m[15] = 1.0f; glUseProgram(program); glUniformMatrix4fv(loc_projection, 1, GL_FALSE, m.data()); glUseProgram(0); } void GLRenderer::flushBatch() { if (batch.empty() || !initialized) { return; } size_t vertexCount = batch.size(); size_t indexCount = indexBatch.size(); size_t vertexBytes = vertexCount * sizeof(Vertex); size_t indexBytes = indexCount * sizeof(GLuint); glUseProgram(program); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); if (vertexCount > STREAM_VBO_CAPACITY || indexCount > STREAM_EBO_CAPACITY) { batch.clear(); indexBatch.clear(); glBindVertexArray(0); glUseProgram(0); return; } if (vboOffset + vertexCount > STREAM_VBO_CAPACITY || eboOffset + indexCount > STREAM_EBO_CAPACITY) { glBufferData(GL_ARRAY_BUFFER, STREAM_VBO_CAPACITY * sizeof(Vertex), nullptr, GL_DYNAMIC_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, STREAM_EBO_CAPACITY * sizeof(GLuint), nullptr, GL_DYNAMIC_DRAW); vboOffset = 0; eboOffset = 0; } void* vboPtr = glMapBufferRange(GL_ARRAY_BUFFER, vboOffset * sizeof(Vertex), vertexBytes, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT | GL_MAP_UNSYNCHRONIZED_BIT); if (!vboPtr) { batch.clear(); indexBatch.clear(); glBindVertexArray(0); glUseProgram(0); return; } std::memcpy(vboPtr, batch.data(), vertexBytes); glUnmapBuffer(GL_ARRAY_BUFFER); void* eboPtr = glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, eboOffset * sizeof(GLuint), indexBytes, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT | GL_MAP_UNSYNCHRONIZED_BIT); if (!eboPtr) { batch.clear(); indexBatch.clear(); glBindVertexArray(0); glUseProgram(0); return; } std::memcpy(eboPtr, indexBatch.data(), indexBytes); glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, current_texture); glUniform1i(loc_texture, 0); glDrawElementsBaseVertex(GL_TRIANGLES, (GLsizei)indexCount, GL_UNSIGNED_INT, (void*)(eboOffset * sizeof(GLuint)), (GLint)vboOffset); vboOffset += vertexCount; eboOffset += indexCount; glBindVertexArray(0); glUseProgram(0); batch.clear(); indexBatch.clear(); } void GLRenderer::drawTexturedQuad(float x, float y, float w, float h, GLuint textureId, const GLColor &color, float u0, float v0_, float u1, float v1_) { DrawCommand cmd; cmd.state.textureId = textureId; cmd.state.blendSrc = activeBlendSrc; cmd.state.blendDst = activeBlendDst; cmd.isQuadBatch = true; cmd.vertices = { { x, y, u0, v0_, color.r, color.g, color.b, color.a }, { x + w, y, u1, v0_, color.r, color.g, color.b, color.a }, { x + w, y + h, u1, v1_, color.r, color.g, color.b, color.a }, { x, y + h, u0, v1_, color.r, color.g, color.b, color.a }, }; commandList.push_back(std::move(cmd)); } void GLRenderer::drawColoredQuad(float x, float y, float w, float h, const GLColor &color) { DrawCommand cmd; cmd.state.textureId = whitePixelTexture; cmd.state.blendSrc = activeBlendSrc; cmd.state.blendDst = activeBlendDst; cmd.isQuadBatch = true; cmd.vertices = { { x, y, 0, 0, color.r, color.g, color.b, color.a }, { x + w, y, 0, 0, color.r, color.g, color.b, color.a }, { x + w, y + h, 0, 0, color.r, color.g, color.b, color.a }, { x, y + h, 0, 0, color.r, color.g, color.b, color.a }, }; commandList.push_back(std::move(cmd)); } void GLRenderer::drawThickLineSegment(float x1, float y1, float x2, float y2, float width, const GLColor &color) { float dx = x2 - x1; float dy = y2 - y1; float len = sqrtf(dx * dx + dy * dy); if (len < 1e-6f) { return; } float nx = (-dy / len) * (width * 0.5f); float ny = (dx / len) * (width * 0.5f); DrawCommand cmd; cmd.state.textureId = whitePixelTexture; cmd.state.blendSrc = activeBlendSrc; cmd.state.blendDst = activeBlendDst; cmd.isQuadBatch = true; cmd.vertices = { { x1 + nx, y1 + ny, 0, 0, color.r, color.g, color.b, color.a }, { x1 - nx, y1 - ny, 0, 0, color.r, color.g, color.b, color.a }, { x2 - nx, y2 - ny, 0, 0, color.r, color.g, color.b, color.a }, { x2 + nx, y2 + ny, 0, 0, color.r, color.g, color.b, color.a }, }; commandList.push_back(std::move(cmd)); } void GLRenderer::drawRect(float x, float y, float w, float h, const GLColor &color, float lineWidth) { drawThickLineSegment(x, y, x + w, y, lineWidth, color); drawThickLineSegment(x + w, y, x + w, y + h, lineWidth, color); drawThickLineSegment(x + w, y + h, x, y + h, lineWidth, color); drawThickLineSegment(x, y + h, x, y, lineWidth, color); } void GLRenderer::drawRoundedRect(float x, float y, float w, float h, float radius, const GLColor &fill) { const int segments = 8; float cx = x + w * 0.5f; float cy = y + h * 0.5f; Vertex center = { cx, cy, 0, 0, fill.r, fill.g, fill.b, fill.a }; std::array, 4> corners = { { { x + radius, y + radius }, { x + w - radius, y + radius }, { x + w - radius, y + h - radius }, { x + radius, y + h - radius }, } }; constexpr float pi = std::numbers::pi_v; std::array startAngle = { pi, 1.5f * pi, 0.0f, 0.5f * pi }; std::vector perimeter; for (int c = 0; c < 4; ++c) { for (int s = 0; s <= segments; ++s) { float angle = startAngle[c] + (s / static_cast(segments)) * (pi * 0.5f); float px = corners[c][0] + cosf(angle) * radius; float py = corners[c][1] + sinf(angle) * radius; perimeter.push_back({ px, py, 0, 0, fill.r, fill.g, fill.b, fill.a }); } } DrawCommand cmd; cmd.state.textureId = whitePixelTexture; cmd.state.blendSrc = activeBlendSrc; cmd.state.blendDst = activeBlendDst; cmd.isQuadBatch = false; for (size_t i = 0; i < perimeter.size(); ++i) { size_t next = (i + 1) % perimeter.size(); cmd.vertices.push_back(center); cmd.vertices.push_back(perimeter[i]); cmd.vertices.push_back(perimeter[next]); } commandList.push_back(std::move(cmd)); } void GLRenderer::drawRoundedRectOutline(float x, float y, float w, float h, float radius, const GLColor &color, float lineWidth) { const int segments = 8; std::array, 4> corners = { { { x + radius, y + radius }, { x + w - radius, y + radius }, { x + w - radius, y + h - radius }, { x + radius, y + h - radius }, } }; constexpr float pi = std::numbers::pi_v; std::array startAngle = { pi, 1.5f * pi, 0.0f, 0.5f * pi }; std::vector> perimeter; for (int c = 0; c < 4; ++c) { for (int s = 0; s <= segments; ++s) { float angle = startAngle[c] + (s / static_cast(segments)) * (pi * 0.5f); float px = corners[c][0] + cosf(angle) * radius; float py = corners[c][1] + sinf(angle) * radius; perimeter.push_back({ px, py }); } } for (size_t i = 0; i < perimeter.size(); ++i) { size_t next = (i + 1) % perimeter.size(); drawThickLineSegment(perimeter[i][0], perimeter[i][1], perimeter[next][0], perimeter[next][1], lineWidth, color); } } void GLRenderer::drawLine(float x1, float y1, float x2, float y2, const GLColor &color, float width) { drawThickLineSegment(x1, y1, x2, y2, width, color); } void GLRenderer::drawLines(const float* vertices, int pairCount, uint8_t r, uint8_t g, uint8_t b, uint8_t a, float width) { GLColor c = { r, g, b, a }; for (int i = 0; i < pairCount; ++i) { float x1 = vertices[i * 4]; float y1 = vertices[i * 4 + 1]; float x2 = vertices[i * 4 + 2]; float y2 = vertices[i * 4 + 3]; drawThickLineSegment(x1, y1, x2, y2, width, c); } } void GLRenderer::drawStippledLines(const float* vertices, int pairCount, const GLColor &color, float width, int factor, uint16_t pattern) { for (int i = 0; i < pairCount; ++i) { float x1 = vertices[i * 4]; float y1 = vertices[i * 4 + 1]; float x2 = vertices[i * 4 + 2]; float y2 = vertices[i * 4 + 3]; float dx = x2 - x1; float dy = y2 - y1; float len = sqrtf(dx * dx + dy * dy); if (len < 1e-6f) { continue; } float dirX = dx / len; float dirY = dy / len; auto step = static_cast(factor); int bit = 0; float pos = 0.0f; while (pos < len) { float segEnd = pos + step; if (segEnd > len) { segEnd = len; } if (pattern & (1 << (bit & 15))) { float sx = x1 + dirX * pos; float sy = y1 + dirY * pos; float ex = x1 + dirX * segEnd; float ey = y1 + dirY * segEnd; drawThickLineSegment(sx, sy, ex, ey, width, color); } pos = segEnd; bit++; } } } void GLRenderer::drawPolygon(const float* vertices, int vertexCount, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { if (vertexCount < 3) { return; } DrawCommand cmd; cmd.state.textureId = whitePixelTexture; cmd.state.blendSrc = activeBlendSrc; cmd.state.blendDst = activeBlendDst; cmd.isQuadBatch = false; for (int i = 1; i < vertexCount - 1; ++i) { cmd.vertices.push_back({ vertices[0], vertices[1], 0, 0, r, g, b, a }); cmd.vertices.push_back({ vertices[i * 2], vertices[i * 2 + 1], 0, 0, r, g, b, a }); cmd.vertices.push_back({ vertices[(i + 1) * 2], vertices[(i + 1) * 2 + 1], 0, 0, r, g, b, a }); } commandList.push_back(std::move(cmd)); } void GLRenderer::drawTriangleFan(const float* vertices, int vertexCount, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { if (vertexCount < 3) { return; } DrawCommand cmd; cmd.state.textureId = whitePixelTexture; cmd.state.blendSrc = activeBlendSrc; cmd.state.blendDst = activeBlendDst; cmd.isQuadBatch = false; for (int i = 1; i < vertexCount - 1; ++i) { cmd.vertices.push_back({ vertices[0], vertices[1], 0, 0, r, g, b, a }); cmd.vertices.push_back({ vertices[i * 2], vertices[i * 2 + 1], 0, 0, r, g, b, a }); cmd.vertices.push_back({ vertices[(i + 1) * 2], vertices[(i + 1) * 2 + 1], 0, 0, r, g, b, a }); } commandList.push_back(std::move(cmd)); } void GLRenderer::drawText(float x, float y, const std::string &text, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { font.textColor = { r, g, b, a }; setRasterPos(x, y); for (char c : text) { drawBitmapChar(c); } } float GLRenderer::getCharWidth(char c) { if (c < 32 || c > 127) { return 0.0f; } return font.advances[c - 32]; } float GLRenderer::getLineHeight() const { return font.lineHeight; } float GLRenderer::getAscent() const { return font.ascent; } void GLRenderer::setRasterPos(float x, float y) { font.cursorX = x; font.cursorY = y; } void GLRenderer::drawBitmapChar(char c) { if (c < 32 || c > 127) { return; } int idx = c - 32; const auto &g = font.glyphs[idx]; float qx = font.cursorX + g.xoff; float qy = font.cursorY + g.yoff; float qw = g.w; float qh = g.h; DrawCommand cmd; cmd.state.textureId = font.texture; cmd.state.blendSrc = activeBlendSrc; cmd.state.blendDst = activeBlendDst; cmd.isQuadBatch = true; cmd.vertices = { { qx, qy, g.u0, g.v0, font.textColor.r, font.textColor.g, font.textColor.b, font.textColor.a }, { qx + qw, qy, g.u1, g.v0, font.textColor.r, font.textColor.g, font.textColor.b, font.textColor.a }, { qx + qw, qy + qh, g.u1, g.v1, font.textColor.r, font.textColor.g, font.textColor.b, font.textColor.a }, { qx, qy + qh, g.u0, g.v1, font.textColor.r, font.textColor.g, font.textColor.b, font.textColor.a }, }; commandList.push_back(std::move(cmd)); font.cursorX += g.advance; } void GLRenderer::setColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { font.textColor = { r, g, b, a }; } void GLRenderer::mergeCommands() { if (commandList.size() <= 1) { return; } size_t write = 0; for (size_t read = 1; read < commandList.size(); ++read) { if (commandList[write].state == commandList[read].state && commandList[write].isQuadBatch == commandList[read].isQuadBatch) { auto &src = commandList[read].vertices; auto &dst = commandList[write].vertices; dst.insert(dst.end(), src.begin(), src.end()); } else { ++write; if (write != read) { commandList[write] = std::move(commandList[read]); } } } commandList.resize(write + 1); } void GLRenderer::flushCommands() { mergeCommands(); unsigned int currentBlendSrc = 0; unsigned int currentBlendDst = 0; for (auto &cmd : commandList) { bool textureChanged = current_texture != cmd.state.textureId; bool blendChanged = cmd.state.blendSrc != currentBlendSrc || cmd.state.blendDst != currentBlendDst; if ((textureChanged || blendChanged) && !batch.empty()) { flushBatch(); } if (blendChanged) { currentBlendSrc = cmd.state.blendSrc; currentBlendDst = cmd.state.blendDst; if (currentBlendSrc != 0) { glBlendFunc(currentBlendSrc, currentBlendDst); } else { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } } current_texture = cmd.state.textureId; // Processes command vertices in slices that fit into the buffer const auto &verts = cmd.vertices; size_t vertStep = cmd.isQuadBatch ? 4 : 3; size_t idxPerStep = cmd.isQuadBatch ? 6 : 3; size_t i = 0; while (i < verts.size()) { size_t remaining = verts.size() - i; size_t batchFree = STREAM_VBO_CAPACITY - batch.size(); // Number of vertices that still fit (rounded to a multiple of vertStep) size_t canTake = (batchFree / vertStep) * vertStep; if (canTake == 0) { flushBatch(); canTake = (STREAM_VBO_CAPACITY / vertStep) * vertStep; } size_t take = std::min(remaining, canTake); if (cmd.isQuadBatch) { auto base = (GLuint)batch.size(); batch.insert(batch.end(), verts.begin() + i, verts.begin() + i + take); for (size_t q = 0; q < take; q += 4) { GLuint b = base + (GLuint)q; indexBatch.push_back(b); indexBatch.push_back(b + 1); indexBatch.push_back(b + 2); indexBatch.push_back(b); indexBatch.push_back(b + 2); indexBatch.push_back(b + 3); } } else { auto base = (GLuint)batch.size(); batch.insert(batch.end(), verts.begin() + i, verts.begin() + i + take); for (GLuint j = 0; j < (GLuint)take; ++j) { indexBatch.push_back(base + j); } } i += take; } } commandList.clear(); flushBatch(); if (currentBlendSrc != 0) { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } } void GLRenderer::setBlendMode(unsigned int src, unsigned int dst) { activeBlendSrc = src; activeBlendDst = dst; } void GLRenderer::resetBlendMode() { activeBlendSrc = 0; activeBlendDst = 0; } void GLRenderer::flush() { flushCommands(); } void GLRenderer::invalidateTexture(GLuint id) { for (auto* inst : s_instances) { if (inst->current_texture == id) { inst->current_texture = 0; } } } void GLRenderer::ensureFBO(int w, int h) { if (fboData.fbo != 0 && fboData.width == w && fboData.height == h) { return; } destroyFBO(); glGenFramebuffers(1, &fboData.fbo); glBindFramebuffer(GL_FRAMEBUFFER, fboData.fbo); glGenTextures(1, &fboData.texture); glBindTexture(GL_TEXTURE_2D, fboData.texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fboData.texture, 0); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { spdlog::error("[GLRenderer::ensureFBO] Framebuffer incomplete"); glDeleteTextures(1, &fboData.texture); glDeleteFramebuffers(1, &fboData.fbo); fboData.fbo = 0; fboData.texture = 0; } glBindFramebuffer(GL_FRAMEBUFFER, 0); fboData.width = w; fboData.height = h; } void GLRenderer::destroyFBO() { if (fboData.texture != 0) { glDeleteTextures(1, &fboData.texture); fboData.texture = 0; } if (fboData.fbo != 0) { glDeleteFramebuffers(1, &fboData.fbo); fboData.fbo = 0; } fboData.width = 0; fboData.height = 0; } void GLRenderer::beginFBO() { if (fboData.fbo != 0) { glBindFramebuffer(GL_FRAMEBUFFER, fboData.fbo); } } void GLRenderer::endFBO() { if (fboData.fbo != 0) { glBindFramebuffer(GL_FRAMEBUFFER, 0); } } void GLRenderer::blitFBO(float w, float h) { if (fboData.fbo == 0) { return; } glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); drawTexturedQuad(0, 0, w, h, fboData.texture, { 255, 255, 255, 255 }, 0.f, 1.f, 1.f, 0.f); flush(); }