fix(D.2b): draw UI sprites in submission order so stamina/mana numbers render

TextRenderer batched sprites per-texture and drew each texture's whole buffer at
its FIRST-insertion point. The dat-font glyph atlas is one shared texture used by
all three vital numbers; it first appeared at the health bar, so all three numbers
were emitted right after the health bars — then the stamina + mana bar sprites
painted over their own numbers (only health survived). Replaced the per-texture
dictionary with submission-ordered segments (consecutive same-texture quads still
batch); each meter's number now draws after its own bars. The renderer's own
comment had predicted this break once bars became sprites (importer did that).
Removed the temporary UiMeter label diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-15 18:27:13 +02:00
parent 8aa643f3e0
commit 43064bab09
2 changed files with 38 additions and 16 deletions

View file

@ -29,7 +29,15 @@ 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();
// 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;
@ -65,7 +73,7 @@ public sealed unsafe class TextRenderer : IDisposable
_screenSize = screenSize;
_textBuf.Clear();
_rectBuf.Clear();
foreach (var b in _spriteBufs.Values) b.Clear();
_segUsed = 0; // pool the SpriteSeg objects across frames
_textVerts = 0;
_rectVerts = 0;
}
@ -139,12 +147,24 @@ public sealed unsafe class TextRenderer : IDisposable
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))
SpriteSeg seg;
if (_segUsed > 0 && _spriteSegs[_segUsed - 1].Texture == texture)
{
buf = new List<float>(256);
_spriteBufs[texture] = buf;
seg = _spriteSegs[_segUsed - 1]; // extend the current same-texture run
}
AppendQuad(buf, x, y, w, h, u0, v0, u1, v1, tint);
else if (_segUsed < _spriteSegs.Count)
{
seg = _spriteSegs[_segUsed++]; // reuse a pooled segment
seg.Texture = texture;
seg.Verts.Clear();
}
else
{
seg = new SpriteSeg { Texture = texture };
_spriteSegs.Add(seg);
_segUsed++;
}
AppendQuad(seg.Verts, x, y, w, h, u0, v0, u1, v1, tint);
}
private static void AppendQuad(List<float> buf,
@ -177,8 +197,7 @@ 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)
{
bool hasSprites = false;
foreach (var b in _spriteBufs.Values) if (b.Count > 0) { hasSprites = true; break; }
bool hasSprites = _segUsed > 0;
if (_textVerts == 0 && _rectVerts == 0 && !hasSprites) return;
_shader.Use();
@ -201,9 +220,10 @@ public sealed unsafe class TextRenderer : IDisposable
// 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
// NOTE: this type-bucketed order is correct while bars are solid rects.
// When bars become gradient SPRITES, this must move to true submission
// (painter) order so sprite-on-sprite z is preserved (D.2b follow-up).
// Bucket 1 (sprites) draws in SUBMISSION (painter) order via _spriteSegs,
// so sprite-on-sprite z is preserved — each meter's dat-font number draws
// after its own bar sprites. Buckets 2 (rects) + 3 (debug text) composite
// on top, in that order.
// 1. RGBA dat sprites first — one draw call per distinct GL texture.
if (hasSprites)
@ -211,12 +231,13 @@ public sealed unsafe class TextRenderer : IDisposable
_shader.SetInt("uUseTexture", 2);
_gl.ActiveTexture(TextureUnit.Texture0);
_shader.SetInt("uTex", 0);
foreach (var kv in _spriteBufs)
for (int i = 0; i < _segUsed; i++)
{
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));
var seg = _spriteSegs[i];
if (seg.Verts.Count == 0) continue;
_gl.BindTexture(TextureTarget.Texture2D, seg.Texture);
UploadBuffer(seg.Verts);
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)(seg.Verts.Count / FloatsPerVertex));
}
}

View file

@ -16,6 +16,7 @@ namespace AcDream.App.UI;
/// </summary>
public sealed class UiMeter : UiElement
{
/// <summary>Fill fraction provider; a null result draws an empty bar.</summary>
public Func<float?> Fill { get; set; } = () => 0f;
/// <summary>Centered overlay text provider (e.g. "291/291"); null = none.</summary>