using System; using System.Collections.Generic; using System.IO; using System.Numerics; using System.Runtime.InteropServices; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// /// 2D batched quad renderer for text + solid rectangles. Coordinates are in /// screen pixels with origin top-left, +X right, +Y down. Call /// at the start of a HUD pass, queue geometry via /// / , then . /// /// Uses two internal vertex buffers (text and rect) flushed in two draw calls /// to avoid a per-vertex "use texture" flag. Rects are drawn first so text /// sits on top of background panels. /// public sealed unsafe class TextRenderer : IDisposable { private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4) private readonly GL _gl; private readonly Shader _shader; private readonly uint _vao; private readonly uint _vbo; private int _vboCapacityBytes; private readonly List _textBuf = new(8192); private readonly List _rectBuf = new(1024); private int _textVerts; private int _rectVerts; private Vector2 _screenSize; public TextRenderer(GL gl, string shaderDir) { _gl = gl; _shader = new Shader(gl, Path.Combine(shaderDir, "ui_text.vert"), Path.Combine(shaderDir, "ui_text.frag")); _vao = _gl.GenVertexArray(); _vbo = _gl.GenBuffer(); _gl.BindVertexArray(_vao); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); uint stride = FloatsPerVertex * sizeof(float); _gl.EnableVertexAttribArray(0); _gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, (void*)0); _gl.EnableVertexAttribArray(1); _gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(2 * sizeof(float))); _gl.EnableVertexAttribArray(2); _gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, stride, (void*)(4 * sizeof(float))); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); _gl.BindVertexArray(0); } /// Begin a HUD pass. Call once per frame before any Draw* calls. public void Begin(Vector2 screenSize) { _screenSize = screenSize; _textBuf.Clear(); _rectBuf.Clear(); _textVerts = 0; _rectVerts = 0; } /// Draw a filled rectangle in screen pixel space. public void DrawRect(float x, float y, float w, float h, Vector4 color) { AppendQuad(_rectBuf, x, y, w, h, 0, 0, 0, 0, color); _rectVerts += 6; } /// Draw a 1-pixel-thick outline rect. public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f) { // top, bottom, left, right DrawRect(x, y, w, thickness, color); DrawRect(x, y + h - thickness, w, thickness, color); DrawRect(x, y, thickness, h, color); DrawRect(x + w - thickness, y, thickness, h, color); } /// /// Draw a single line of text at (x,y) where (x,y) is the top-left of the /// typographic block. Handles '\n' as a line break. /// public void DrawString(BitmapFont font, string text, float x, float y, Vector4 color) { float cursorX = x; // The caller provides top-y; shift to baseline for glyph offset math. float baseline = y + font.Ascent; for (int i = 0; i < text.Length; i++) { char c = text[i]; if (c == '\n') { cursorX = x; baseline += font.LineHeight; continue; } if (!font.TryGetGlyph(c, out var g)) { // Unknown glyph — skip its advance width if '?' exists. if (font.TryGetGlyph('?', out var q)) cursorX += q.Advance; continue; } float gx = cursorX + g.OffsetX; float gy = baseline + g.OffsetY; float gw = g.Width; float gh = g.Height; if (gw > 0 && gh > 0) { AppendQuad(_textBuf, gx, gy, gw, gh, g.UvMinX, g.UvMinY, g.UvMaxX, g.UvMaxY, color); _textVerts += 6; } cursorX += g.Advance; } } private static void AppendQuad(List buf, float x, float y, float w, float h, float u0, float v0, float u1, float v1, Vector4 color) { // Two triangles (6 verts). CCW in pixel space is clockwise in NDC // because the vertex shader flips Y, so OpenGL's default front-face // is GL_CCW — we rely on cull-face being disabled during HUD pass. // (x, y) ─ (x+w, y) // │ │ // (x, y+h) ─ (x+w, y+h) // // Triangle 1: (x,y) (x+w,y+h) (x+w,y) // Triangle 2: (x,y) (x,y+h) (x+w,y+h) void V(float px, float py, float pu, float pv) { buf.Add(px); buf.Add(py); buf.Add(pu); buf.Add(pv); buf.Add(color.X); buf.Add(color.Y); buf.Add(color.Z); buf.Add(color.W); } V(x, y, u0, v0); V(x + w, y + h, u1, v1); V(x + w, y, u1, v0); V(x, y, u0, v0); V(x, y + h, u0, v1); V(x + w, y + h, u1, v1); } /// Upload + draw accumulated rects + text. font may be null if only DrawRect was used. public void Flush(BitmapFont? font) { if (_textVerts == 0 && _rectVerts == 0) return; _shader.Use(); _shader.SetVec2("uScreenSize", _screenSize); _gl.BindVertexArray(_vao); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); // Save GL state. bool wasDepth = _gl.IsEnabled(EnableCap.DepthTest); bool wasBlend = _gl.IsEnabled(EnableCap.Blend); bool wasCull = _gl.IsEnabled(EnableCap.CullFace); _gl.Disable(EnableCap.DepthTest); _gl.Disable(EnableCap.CullFace); _gl.Enable(EnableCap.Blend); _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); // Untextured rects first — they form panel backgrounds. if (_rectVerts > 0) { _shader.SetInt("uUseTexture", 0); UploadBuffer(_rectBuf); _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)_rectVerts); } // Textured text glyphs. if (_textVerts > 0 && font is not null) { _shader.SetInt("uUseTexture", 1); _gl.ActiveTexture(TextureUnit.Texture0); _gl.BindTexture(TextureTarget.Texture2D, font.TextureId); _shader.SetInt("uTex", 0); UploadBuffer(_textBuf); _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)_textVerts); } // Restore GL state. if (!wasBlend) _gl.Disable(EnableCap.Blend); if (wasCull) _gl.Enable(EnableCap.CullFace); if (wasDepth) _gl.Enable(EnableCap.DepthTest); _gl.BindVertexArray(0); } private void UploadBuffer(List buf) { int bytes = buf.Count * sizeof(float); if (bytes == 0) return; if (bytes > _vboCapacityBytes) { fixed (float* p = CollectionsMarshal.AsSpan(buf)) _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)bytes, p, BufferUsageARB.DynamicDraw); _vboCapacityBytes = bytes; } else { fixed (float* p = CollectionsMarshal.AsSpan(buf)) _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)bytes, p); } } public void Dispose() { _gl.DeleteBuffer(_vbo); _gl.DeleteVertexArray(_vao); _shader.Dispose(); } }