Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
494 lines
20 KiB
C#
494 lines
20 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Numerics;
|
||
using System.Runtime.InteropServices;
|
||
using AcDream.App.Rendering.Wb;
|
||
using Silk.NET.OpenGL;
|
||
|
||
namespace AcDream.App.Rendering;
|
||
|
||
/// <summary>
|
||
/// 2D batched quad renderer for text + solid rectangles. Coordinates are in
|
||
/// screen pixels with origin top-left, +X right, +Y down. Call
|
||
/// <see cref="Begin"/> at the start of a HUD pass, queue geometry via
|
||
/// <see cref="DrawString"/> / <see cref="DrawRect"/>, then <see cref="Flush"/>.
|
||
///
|
||
/// 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.
|
||
/// </summary>
|
||
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 uint _vao;
|
||
private uint _vbo;
|
||
private readonly uint _whiteTex; // 1×1 white, for solid fills routed through the sprite bucket
|
||
private int _vboCapacityBytes;
|
||
|
||
private sealed class FrameBufferSet
|
||
{
|
||
public uint Vao;
|
||
public uint Vbo;
|
||
public int CapacityBytes;
|
||
public int UsedBytes;
|
||
}
|
||
|
||
private readonly FrameBufferSet[] _frameBuffers = new FrameBufferSet[3];
|
||
private FrameBufferSet? _activeFrameBuffer;
|
||
|
||
internal long DynamicBufferCapacityBytes =>
|
||
_frameBuffers.Sum(set => (long)set.CapacityBytes);
|
||
|
||
private readonly List<float> _textBuf = new(8192);
|
||
private readonly List<float> _rectBuf = new(1024);
|
||
// Submission-ordered sprite segments: consecutive DrawSprite calls with the
|
||
// SAME texture batch into one segment; a texture change starts a new segment.
|
||
// Drawing segments in submission order preserves painter z-order for
|
||
// sprite-on-sprite UI. (The old per-texture dictionary drew a REUSED texture
|
||
// at its FIRST-insertion point, so later bar sprites covered glyphs emitted
|
||
// earlier via the shared dat-font atlas — the stamina/mana numbers vanished.)
|
||
private sealed class SpriteSeg { public uint Texture; public readonly List<float> Verts = new(256); }
|
||
private readonly List<SpriteSeg> _spriteSegs = new();
|
||
private int _segUsed;
|
||
private int _textVerts;
|
||
private int _rectVerts;
|
||
private Vector2 _screenSize;
|
||
|
||
// Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text
|
||
// buckets, so open popups/menus composite on top of EVERYTHING, including translucent
|
||
// rect panel backgrounds (which otherwise always win because rects flush after
|
||
// sprites). Routed by OverlayMode; the UI root sets it for the popup traversal.
|
||
private readonly List<float> _overlayTextBuf = new(1024);
|
||
private readonly List<float> _overlayRectBuf = new(256);
|
||
private readonly List<SpriteSeg> _overlaySpriteSegs = new();
|
||
private int _overlaySegUsed;
|
||
private int _overlayTextVerts;
|
||
private int _overlayRectVerts;
|
||
|
||
/// <summary>When true, Draw* calls route to the overlay layer (flushed last, on top
|
||
/// of all normal-layer geometry). Set by the UI root around the popup/overlay pass.</summary>
|
||
public bool OverlayMode { get; set; }
|
||
|
||
public TextRenderer(GL gl, string shaderDir)
|
||
{
|
||
_gl = gl;
|
||
_shader = new Shader(gl,
|
||
Path.Combine(shaderDir, "ui_text.vert"),
|
||
Path.Combine(shaderDir, "ui_text.frag"));
|
||
|
||
for (int i = 0; i < _frameBuffers.Length; i++)
|
||
_frameBuffers[i] = CreateFrameBufferSet();
|
||
|
||
// 1×1 white texture so DrawFill can route solid-colour quads through the SPRITE
|
||
// bucket (the shader multiplies texel×color → white×color = color). Lets a panel
|
||
// background draw UNDER its text in painter order, which DrawRect's separate
|
||
// bucket cannot (it always composites after all sprites).
|
||
_whiteTex = _gl.GenTexture();
|
||
_gl.BindTexture(TextureTarget.Texture2D, _whiteTex);
|
||
Span<byte> whitePixel = stackalloc byte[] { 255, 255, 255, 255 };
|
||
fixed (byte* wp = whitePixel)
|
||
_gl.TexImage2D(TextureTarget.Texture2D, 0, (int)InternalFormat.Rgba8, 1, 1, 0,
|
||
PixelFormat.Rgba, PixelType.UnsignedByte, wp);
|
||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
|
||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Nearest);
|
||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Selects the GPU-fenced frame slot and resets its append cursor. Every
|
||
/// UI segment rendered during the frame receives a distinct byte range;
|
||
/// later text or sprite batches cannot overwrite an earlier in-flight draw.
|
||
/// </summary>
|
||
public void BeginFrame(int frameSlot)
|
||
{
|
||
if ((uint)frameSlot >= (uint)_frameBuffers.Length)
|
||
throw new ArgumentOutOfRangeException(nameof(frameSlot));
|
||
|
||
FrameBufferSet set = _frameBuffers[frameSlot];
|
||
set.UsedBytes = 0;
|
||
_activeFrameBuffer = set;
|
||
_vao = set.Vao;
|
||
_vbo = set.Vbo;
|
||
_vboCapacityBytes = set.CapacityBytes;
|
||
}
|
||
|
||
private FrameBufferSet CreateFrameBufferSet()
|
||
{
|
||
uint vao = 0;
|
||
uint vbo = 0;
|
||
try
|
||
{
|
||
vao = TrackedGlResource.CreateVertexArray(
|
||
_gl,
|
||
"TextRenderer frame VAO creation");
|
||
vbo = TrackedGlResource.CreateBuffer(
|
||
_gl,
|
||
"TextRenderer frame VBO creation");
|
||
var set = new FrameBufferSet { Vao = vao, Vbo = vbo };
|
||
|
||
_gl.BindVertexArray(set.Vao);
|
||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.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);
|
||
return set;
|
||
}
|
||
catch
|
||
{
|
||
if (vbo != 0)
|
||
TrackedGlResource.DeleteBuffer(
|
||
_gl, vbo, 0, "TextRenderer frame VBO rollback");
|
||
if (vao != 0)
|
||
TrackedGlResource.DeleteVertexArray(
|
||
_gl, vao, "TextRenderer frame VAO rollback");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>Begin a HUD pass. Call once per frame before any Draw* calls.</summary>
|
||
public void Begin(Vector2 screenSize)
|
||
{
|
||
_screenSize = screenSize;
|
||
_textBuf.Clear();
|
||
_rectBuf.Clear();
|
||
_segUsed = 0; // pool the SpriteSeg objects across frames
|
||
_textVerts = 0;
|
||
_rectVerts = 0;
|
||
_overlayTextBuf.Clear();
|
||
_overlayRectBuf.Clear();
|
||
_overlaySegUsed = 0;
|
||
_overlayTextVerts = 0;
|
||
_overlayRectVerts = 0;
|
||
OverlayMode = false;
|
||
}
|
||
|
||
/// <summary>Draw a filled rectangle in screen pixel space.</summary>
|
||
public void DrawRect(float x, float y, float w, float h, Vector4 color)
|
||
{
|
||
if (OverlayMode) { AppendQuad(_overlayRectBuf, x, y, w, h, 0, 0, 0, 0, color); _overlayRectVerts += 6; }
|
||
else { AppendQuad(_rectBuf, x, y, w, h, 0, 0, 0, 0, color); _rectVerts += 6; }
|
||
}
|
||
|
||
/// <summary>Draw a solid-colour quad through the SPRITE bucket (and the overlay layer
|
||
/// when active), so it composites in painter order with sprites + dat-font text. Use
|
||
/// this — not <see cref="DrawRect"/> — for a panel BACKGROUND that text draws on top of:
|
||
/// DrawRect's bucket always flushes after all sprites, so a rect background would cover
|
||
/// the text instead.</summary>
|
||
public void DrawFill(float x, float y, float w, float h, Vector4 color)
|
||
=> DrawSprite(_whiteTex, x, y, w, h, 0f, 0f, 1f, 1f, color);
|
||
|
||
/// <summary>Draw a 1-pixel-thick outline rect.</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public void DrawString(BitmapFont font, string text, float x, float y, Vector4 color)
|
||
=> DrawStringCore(
|
||
font, text, x, y, color,
|
||
clip: false, 0f, 0f, 0f, 0f);
|
||
|
||
/// <summary>
|
||
/// Draw a bitmap-font string clipped to an absolute screen-space rectangle.
|
||
/// The retained UI uses this overload when a text element intersects its authored
|
||
/// surface edge. Retail <c>UIElement_Text::DrawSelf @ 0x00467AA0</c> clips the
|
||
/// individual glyph blits instead of discarding the whole line.
|
||
/// </summary>
|
||
internal void DrawStringClipped(
|
||
BitmapFont font,
|
||
string text,
|
||
float x,
|
||
float y,
|
||
Vector4 color,
|
||
float clipLeft,
|
||
float clipTop,
|
||
float clipRight,
|
||
float clipBottom)
|
||
=> DrawStringCore(
|
||
font, text, x, y, color,
|
||
clip: true, clipLeft, clipTop, clipRight, clipBottom);
|
||
|
||
private void DrawStringCore(
|
||
BitmapFont font,
|
||
string text,
|
||
float x,
|
||
float y,
|
||
Vector4 color,
|
||
bool clip,
|
||
float clipLeft,
|
||
float clipTop,
|
||
float clipRight,
|
||
float clipBottom)
|
||
{
|
||
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;
|
||
float u0 = g.UvMinX;
|
||
float v0 = g.UvMinY;
|
||
float u1 = g.UvMaxX;
|
||
float v1 = g.UvMaxY;
|
||
|
||
if (gw > 0 && gh > 0
|
||
&& (!clip || QuadClipper.TryClip(
|
||
clipLeft, clipTop, clipRight, clipBottom,
|
||
ref gx, ref gy, ref gw, ref gh,
|
||
ref u0, ref v0, ref u1, ref v1)))
|
||
{
|
||
if (OverlayMode) { AppendQuad(_overlayTextBuf, gx, gy, gw, gh, u0, v0, u1, v1, color); _overlayTextVerts += 6; }
|
||
else { AppendQuad(_textBuf, gx, gy, gw, gh, u0, v0, u1, v1, color); _textVerts += 6; }
|
||
}
|
||
cursorX += g.Advance;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Draw a textured sprite quad in screen pixel space with an explicit
|
||
/// source-UV rectangle (for 9-slice / atlas sub-regions). Batched per
|
||
/// GL texture handle; flushed with uUseTexture=2 (RGBA modulate).
|
||
/// </summary>
|
||
public void DrawSprite(uint texture, float x, float y, float w, float h,
|
||
float u0, float v0, float u1, float v1, Vector4 tint)
|
||
{
|
||
SpriteSeg seg = OverlayMode
|
||
? NextSpriteSeg(_overlaySpriteSegs, ref _overlaySegUsed, texture)
|
||
: NextSpriteSeg(_spriteSegs, ref _segUsed, texture);
|
||
AppendQuad(seg.Verts, x, y, w, h, u0, v0, u1, v1, tint);
|
||
}
|
||
|
||
/// <summary>Pick the sprite segment for <paramref name="texture"/>: extend the current
|
||
/// same-texture run, else reuse a pooled segment, else allocate. Submission order is
|
||
/// preserved (painter z-order for sprite-on-sprite UI).</summary>
|
||
private static SpriteSeg NextSpriteSeg(List<SpriteSeg> segs, ref int used, uint texture)
|
||
{
|
||
if (used > 0 && segs[used - 1].Texture == texture)
|
||
return segs[used - 1];
|
||
if (used < segs.Count)
|
||
{
|
||
var s = segs[used++];
|
||
s.Texture = texture;
|
||
s.Verts.Clear();
|
||
return s;
|
||
}
|
||
var ns = new SpriteSeg { Texture = texture };
|
||
segs.Add(ns);
|
||
used++;
|
||
return ns;
|
||
}
|
||
|
||
private static void AppendQuad(List<float> 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);
|
||
}
|
||
|
||
/// <summary>Upload + draw accumulated rects + text. font may be null if only DrawRect was used.</summary>
|
||
public void Flush(BitmapFont? font)
|
||
{
|
||
bool anyNormal = _segUsed > 0 || _textVerts > 0 || _rectVerts > 0;
|
||
bool anyOverlay = _overlaySegUsed > 0 || _overlayTextVerts > 0 || _overlayRectVerts > 0;
|
||
if (!anyNormal && !anyOverlay) 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);
|
||
// The world pass leaves alpha-to-coverage + multisample enabled (WbDrawDispatcher,
|
||
// QualitySettings MSAA). If they bleed into the UI pass, each glyph's soft alpha
|
||
// EDGE is converted to dithered MSAA coverage instead of a clean alpha blend —
|
||
// the "text not sharp / fuzzy" artifact. The UI composites with straight alpha
|
||
// blending and must own this state (feedback_render_self_contained_gl_state).
|
||
bool wasA2C = _gl.IsEnabled(EnableCap.SampleAlphaToCoverage);
|
||
bool wasMsaa = _gl.IsEnabled(EnableCap.Multisample);
|
||
_gl.Disable(EnableCap.SampleAlphaToCoverage);
|
||
_gl.Disable(EnableCap.Multisample);
|
||
_gl.Disable(EnableCap.DepthTest);
|
||
_gl.Disable(EnableCap.CullFace);
|
||
_gl.DepthMask(false);
|
||
_gl.Enable(EnableCap.Blend);
|
||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||
|
||
// LAYERED compositing for the UI (background → fill → text):
|
||
// 1. RGBA dat sprites — window chrome / panel backgrounds (behind)
|
||
// 2. Untextured rects — widget fills (e.g. vital bars) on the chrome
|
||
// 3. Text glyphs — on top
|
||
// Bucket 1 (sprites) draws in SUBMISSION (painter) order via _spriteSegs,
|
||
// so sprite-on-sprite z is preserved. Buckets 2 (rects) + 3 (debug text)
|
||
// composite on top, in that order. The OVERLAY layer repeats all three
|
||
// AFTER the normal layer, so open popups beat even the rect backgrounds.
|
||
DrawLayer(_spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font);
|
||
DrawLayer(_overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font);
|
||
|
||
// Restore GL state.
|
||
_gl.DepthMask(true);
|
||
if (!wasBlend) _gl.Disable(EnableCap.Blend);
|
||
if (wasCull) _gl.Enable(EnableCap.CullFace);
|
||
if (wasDepth) _gl.Enable(EnableCap.DepthTest);
|
||
if (wasA2C) _gl.Enable(EnableCap.SampleAlphaToCoverage);
|
||
if (wasMsaa) _gl.Enable(EnableCap.Multisample);
|
||
|
||
_gl.BindVertexArray(0);
|
||
}
|
||
|
||
/// <summary>Draw one compositing layer: sprites (submission order, one call per
|
||
/// texture) → untextured rects → debug-font text. Shared by the normal and overlay
|
||
/// layers; GL state + shader are set up by <see cref="Flush"/>.</summary>
|
||
private void DrawLayer(
|
||
List<SpriteSeg> spriteSegs, int segUsed,
|
||
List<float> rectBuf, int rectVerts,
|
||
List<float> textBuf, int textVerts, BitmapFont? font)
|
||
{
|
||
// 1. RGBA dat sprites — one draw call per distinct GL texture.
|
||
if (segUsed > 0)
|
||
{
|
||
_shader.SetInt("uUseTexture", 2);
|
||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||
_shader.SetInt("uTex", 0);
|
||
for (int i = 0; i < segUsed; i++)
|
||
{
|
||
var seg = spriteSegs[i];
|
||
if (seg.Verts.Count == 0) continue;
|
||
_gl.BindTexture(TextureTarget.Texture2D, seg.Texture);
|
||
int firstVertex = UploadBuffer(seg.Verts);
|
||
_gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)(seg.Verts.Count / FloatsPerVertex));
|
||
}
|
||
}
|
||
|
||
// 2. Untextured rects — widget fills on top of the chrome.
|
||
if (rectVerts > 0)
|
||
{
|
||
_shader.SetInt("uUseTexture", 0);
|
||
int firstVertex = UploadBuffer(rectBuf);
|
||
_gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)rectVerts);
|
||
}
|
||
|
||
// 3. Textured debug-font text glyphs on top.
|
||
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);
|
||
int firstVertex = UploadBuffer(textBuf);
|
||
_gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)textVerts);
|
||
}
|
||
}
|
||
|
||
private int UploadBuffer(List<float> buf)
|
||
{
|
||
int bytes = buf.Count * sizeof(float);
|
||
if (bytes == 0) return 0;
|
||
FrameBufferSet set = _activeFrameBuffer
|
||
?? throw new InvalidOperationException("BeginFrame must be called before rendering text.");
|
||
int byteOffset = set.UsedBytes;
|
||
int requiredBytes = checked(byteOffset + bytes);
|
||
|
||
if (requiredBytes > _vboCapacityBytes)
|
||
{
|
||
int newCapacity = DynamicBufferCapacity.Grow(
|
||
_vboCapacityBytes,
|
||
requiredBytes);
|
||
TrackedGlResource.AllocateBufferStorage(
|
||
_gl,
|
||
GLEnum.ArrayBuffer,
|
||
_vbo,
|
||
_vboCapacityBytes,
|
||
newCapacity,
|
||
GLEnum.DynamicDraw,
|
||
"TextRenderer frame VBO growth");
|
||
_vboCapacityBytes = newCapacity;
|
||
}
|
||
|
||
fixed (float* p = CollectionsMarshal.AsSpan(buf))
|
||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, (nint)byteOffset, (nuint)bytes, p);
|
||
|
||
set.UsedBytes = requiredBytes;
|
||
set.CapacityBytes = _vboCapacityBytes;
|
||
return byteOffset / (FloatsPerVertex * sizeof(float));
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_gl.DeleteTexture(_whiteTex);
|
||
foreach (FrameBufferSet set in _frameBuffers)
|
||
{
|
||
TrackedGlResource.DeleteBuffer(
|
||
_gl,
|
||
set.Vbo,
|
||
set.CapacityBytes,
|
||
"TextRenderer frame VBO disposal");
|
||
TrackedGlResource.DeleteVertexArray(
|
||
_gl,
|
||
set.Vao,
|
||
"TextRenderer frame VAO disposal");
|
||
}
|
||
_shader.Dispose();
|
||
}
|
||
}
|