From 43064bab0989a9e83468a14d5f9df4f040c2c9ab Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 15 Jun 2026 18:27:13 +0200 Subject: [PATCH] fix(D.2b): draw UI sprites in submission order so stamina/mana numbers render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.App/Rendering/TextRenderer.cs | 53 ++++++++++++++++------- src/AcDream.App/UI/UiMeter.cs | 1 + 2 files changed, 38 insertions(+), 16 deletions(-) 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.