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

@ -7,10 +7,13 @@ uniform sampler2D uTex;
uniform int uUseTexture; uniform int uUseTexture;
void main() { void main() {
if (uUseTexture != 0) { if (uUseTexture == 1) {
// Font atlas is a single-channel R8 texture; red = coverage alpha. // Font atlas is a single-channel R8 texture; red = coverage alpha.
float coverage = texture(uTex, vUv).r; float coverage = texture(uTex, vUv).r;
FragColor = vec4(vColor.rgb, vColor.a * coverage); FragColor = vec4(vColor.rgb, vColor.a * coverage);
} else if (uUseTexture == 2) {
// RGBA dat sprite (decoded to RGBA8); modulate by tint/alpha.
FragColor = texture(uTex, vUv) * vColor;
} else { } else {
FragColor = vColor; FragColor = vColor;
} }

View file

@ -29,6 +29,7 @@ public sealed unsafe class TextRenderer : IDisposable
private readonly List<float> _textBuf = new(8192); private readonly List<float> _textBuf = new(8192);
private readonly List<float> _rectBuf = new(1024); private readonly List<float> _rectBuf = new(1024);
private readonly Dictionary<uint, List<float>> _spriteBufs = new();
private int _textVerts; private int _textVerts;
private int _rectVerts; private int _rectVerts;
private Vector2 _screenSize; private Vector2 _screenSize;
@ -64,6 +65,7 @@ public sealed unsafe class TextRenderer : IDisposable
_screenSize = screenSize; _screenSize = screenSize;
_textBuf.Clear(); _textBuf.Clear();
_rectBuf.Clear(); _rectBuf.Clear();
foreach (var b in _spriteBufs.Values) b.Clear();
_textVerts = 0; _textVerts = 0;
_rectVerts = 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, private static void AppendQuad(List<float> buf,
float x, float y, float w, float h, float x, float y, float w, float h,
float u0, float v0, float u1, float v1, Vector4 color) 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> /// <summary>Upload + draw accumulated rects + text. font may be null if only DrawRect was used.</summary>
public void Flush(BitmapFont? font) 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.Use();
_shader.SetVec2("uScreenSize", _screenSize); _shader.SetVec2("uScreenSize", _screenSize);
@ -173,6 +193,7 @@ public sealed unsafe class TextRenderer : IDisposable
bool wasCull = _gl.IsEnabled(EnableCap.CullFace); bool wasCull = _gl.IsEnabled(EnableCap.CullFace);
_gl.Disable(EnableCap.DepthTest); _gl.Disable(EnableCap.DepthTest);
_gl.Disable(EnableCap.CullFace); _gl.Disable(EnableCap.CullFace);
_gl.DepthMask(false);
_gl.Enable(EnableCap.Blend); _gl.Enable(EnableCap.Blend);
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
@ -195,7 +216,23 @@ public sealed unsafe class TextRenderer : IDisposable
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)_textVerts); _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. // Restore GL state.
_gl.DepthMask(true);
if (!wasBlend) _gl.Disable(EnableCap.Blend); if (!wasBlend) _gl.Disable(EnableCap.Blend);
if (wasCull) _gl.Enable(EnableCap.CullFace); if (wasCull) _gl.Enable(EnableCap.CullFace);
if (wasDepth) _gl.Enable(EnableCap.DepthTest); if (wasDepth) _gl.Enable(EnableCap.DepthTest);

View file

@ -14,6 +14,7 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
private readonly GL _gl; private readonly GL _gl;
private readonly DatCollection _dats; private readonly DatCollection _dats;
private readonly Dictionary<uint, uint> _handlesBySurfaceId = new(); private readonly Dictionary<uint, uint> _handlesBySurfaceId = new();
private readonly Dictionary<uint, (int w, int h)> _sizeBySurfaceId = new();
/// <summary> /// <summary>
/// Composite cache for surface-with-override-origtex entries (Phase 5 /// Composite cache for surface-with-override-origtex entries (Phase 5
/// TextureChanges). Key = (baseSurfaceId, overrideOrigTextureId), /// TextureChanges). Key = (baseSurfaceId, overrideOrigTextureId),
@ -80,6 +81,28 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
return h; return h;
} }
/// <summary>
/// Like <see cref="GetOrUpload(uint)"/> but also returns the decoded
/// pixel dimensions. UI 9-slice geometry needs the source size to
/// compute slice UVs. Cached alongside the handle.
/// </summary>
public uint GetOrUpload(uint surfaceId, out int width, out int height)
{
if (_handlesBySurfaceId.TryGetValue(surfaceId, out var existing)
&& _sizeBySurfaceId.TryGetValue(surfaceId, out var sz))
{
width = sz.w; height = sz.h;
return existing;
}
var decoded = DecodeFromDats(surfaceId, origTextureOverride: null, paletteOverride: null);
uint h = UploadRgba8(decoded);
_handlesBySurfaceId[surfaceId] = h;
_sizeBySurfaceId[surfaceId] = (decoded.Width, decoded.Height);
width = decoded.Width; height = decoded.Height;
return h;
}
/// <summary> /// <summary>
/// Alpha-channel histogram for one decoded texture. Used to diagnose /// Alpha-channel histogram for one decoded texture. Used to diagnose
/// "why are clouds not transparent" — if cloud textures come out with /// "why are clouds not transparent" — if cloud textures come out with

View file

@ -53,6 +53,11 @@ public sealed class UiRenderContext
public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f) public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f)
=> TextRenderer.DrawRectOutline(_current.X + x, _current.Y + y, w, h, color, thickness); => TextRenderer.DrawRectOutline(_current.X + x, _current.Y + y, w, h, color, thickness);
public void DrawSprite(uint texture, float x, float y, float w, float h,
float u0, float v0, float u1, float v1, Vector4 tint)
=> TextRenderer.DrawSprite(texture,
_current.X + x, _current.Y + y, w, h, u0, v0, u1, v1, tint);
public void DrawString(string text, float x, float y, Vector4 color, BitmapFont? font = null) public void DrawString(string text, float x, float y, Vector4 color, BitmapFont? font = null)
{ {
var f = font ?? DefaultFont; var f = font ?? DefaultFont;