feat(D.2b): textured-sprite path in TextRenderer + UV-rect DrawSprite

Add uUseTexture==2 (RGBA modulate) branch to ui_text.frag so dat sprites
can be drawn through the existing 2D batcher without touching the font path.

TextRenderer gains _spriteBufs (per-GL-handle List<float>), DrawSprite(), and
a Flush block that issues one draw call per distinct texture with uUseTexture=2.
Also adds DepthMask(false) in the state-save block (restored to true after) to
prevent the transparent-quad pass from writing depth and corrupting the 3D scene
if the UI is flushed mid-frame.

TextureCache gains GetOrUpload(surfaceId, out width, out height) — caches pixel
dimensions alongside the GL handle so UI 9-slice geometry can compute slice UVs
from the source image size without a second decode.

UiRenderContext gains a DrawSprite forwarder that applies the current 2D
translate stack, matching the DrawRect / DrawRectOutline pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-14 14:28:29 +02:00
parent 626d06ebc1
commit c9eef1d7cd
4 changed files with 70 additions and 2 deletions

View file

@ -29,6 +29,7 @@ public sealed unsafe class TextRenderer : IDisposable
private readonly List<float> _textBuf = new(8192);
private readonly List<float> _rectBuf = new(1024);
private readonly Dictionary<uint, List<float>> _spriteBufs = new();
private int _textVerts;
private int _rectVerts;
private Vector2 _screenSize;
@ -64,6 +65,7 @@ public sealed unsafe class TextRenderer : IDisposable
_screenSize = screenSize;
_textBuf.Clear();
_rectBuf.Clear();
foreach (var b in _spriteBufs.Values) b.Clear();
_textVerts = 0;
_rectVerts = 0;
}
@ -129,6 +131,22 @@ public sealed unsafe class TextRenderer : IDisposable
}
}
/// <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)
{
if (!_spriteBufs.TryGetValue(texture, out var buf))
{
buf = new List<float>(256);
_spriteBufs[texture] = buf;
}
AppendQuad(buf, x, y, w, h, u0, v0, u1, v1, tint);
}
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)
@ -159,7 +177,9 @@ public sealed unsafe class TextRenderer : IDisposable
/// <summary>Upload + draw accumulated rects + text. font may be null if only DrawRect was used.</summary>
public void Flush(BitmapFont? font)
{
if (_textVerts == 0 && _rectVerts == 0) return;
bool hasSprites = false;
foreach (var b in _spriteBufs.Values) if (b.Count > 0) { hasSprites = true; break; }
if (_textVerts == 0 && _rectVerts == 0 && !hasSprites) return;
_shader.Use();
_shader.SetVec2("uScreenSize", _screenSize);
@ -173,6 +193,7 @@ public sealed unsafe class TextRenderer : IDisposable
bool wasCull = _gl.IsEnabled(EnableCap.CullFace);
_gl.Disable(EnableCap.DepthTest);
_gl.Disable(EnableCap.CullFace);
_gl.DepthMask(false);
_gl.Enable(EnableCap.Blend);
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
@ -195,7 +216,23 @@ public sealed unsafe class TextRenderer : IDisposable
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)_textVerts);
}
// RGBA dat sprites — one draw call per distinct GL texture.
if (hasSprites)
{
_shader.SetInt("uUseTexture", 2);
_gl.ActiveTexture(TextureUnit.Texture0);
_shader.SetInt("uTex", 0);
foreach (var kv in _spriteBufs)
{
if (kv.Value.Count == 0) continue;
_gl.BindTexture(TextureTarget.Texture2D, kv.Key);
UploadBuffer(kv.Value);
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)(kv.Value.Count / FloatsPerVertex));
}
}
// Restore GL state.
_gl.DepthMask(true);
if (!wasBlend) _gl.Disable(EnableCap.Blend);
if (wasCull) _gl.Enable(EnableCap.CullFace);
if (wasDepth) _gl.Enable(EnableCap.DepthTest);