diff --git a/src/AcDream.App/Rendering/TextRenderer.cs b/src/AcDream.App/Rendering/TextRenderer.cs index a0252518..bef2e2ca 100644 --- a/src/AcDream.App/Rendering/TextRenderer.cs +++ b/src/AcDream.App/Rendering/TextRenderer.cs @@ -29,7 +29,15 @@ public sealed unsafe class TextRenderer : IDisposable private readonly List _textBuf = new(8192); private readonly List _rectBuf = new(1024); - private readonly Dictionary> _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 Verts = new(256); } + private readonly List _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(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 buf, @@ -177,8 +197,7 @@ public sealed unsafe class TextRenderer : IDisposable /// Upload + draw accumulated rects + text. font may be null if only DrawRect was used. 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)); } } diff --git a/src/AcDream.App/UI/UiMeter.cs b/src/AcDream.App/UI/UiMeter.cs index bb5bb55b..f93737a3 100644 --- a/src/AcDream.App/UI/UiMeter.cs +++ b/src/AcDream.App/UI/UiMeter.cs @@ -16,6 +16,7 @@ namespace AcDream.App.UI; /// public sealed class UiMeter : UiElement { + /// Fill fraction provider; a null result draws an empty bar. public Func Fill { get; set; } = () => 0f; /// Centered overlay text provider (e.g. "291/291"); null = none.