From c9eef1d7cd20de74da74fda7232e8233eb6adf49 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 14 Jun 2026 14:28:29 +0200 Subject: [PATCH] feat(D.2b): textured-sprite path in TextRenderer + UV-rect DrawSprite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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), 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) --- .../Rendering/Shaders/ui_text.frag | 5 ++- src/AcDream.App/Rendering/TextRenderer.cs | 39 ++++++++++++++++++- src/AcDream.App/Rendering/TextureCache.cs | 23 +++++++++++ src/AcDream.App/UI/UiRenderContext.cs | 5 +++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/AcDream.App/Rendering/Shaders/ui_text.frag b/src/AcDream.App/Rendering/Shaders/ui_text.frag index 7740ea11..75c9cd3d 100644 --- a/src/AcDream.App/Rendering/Shaders/ui_text.frag +++ b/src/AcDream.App/Rendering/Shaders/ui_text.frag @@ -7,10 +7,13 @@ uniform sampler2D uTex; uniform int uUseTexture; void main() { - if (uUseTexture != 0) { + if (uUseTexture == 1) { // Font atlas is a single-channel R8 texture; red = coverage alpha. float coverage = texture(uTex, vUv).r; 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 { FragColor = vColor; } diff --git a/src/AcDream.App/Rendering/TextRenderer.cs b/src/AcDream.App/Rendering/TextRenderer.cs index ad04da1a..b07a9d40 100644 --- a/src/AcDream.App/Rendering/TextRenderer.cs +++ b/src/AcDream.App/Rendering/TextRenderer.cs @@ -29,6 +29,7 @@ public sealed unsafe class TextRenderer : IDisposable private readonly List _textBuf = new(8192); private readonly List _rectBuf = new(1024); + private readonly Dictionary> _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 } } + /// + /// 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). + /// + 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(256); + _spriteBufs[texture] = buf; + } + AppendQuad(buf, x, y, w, h, u0, v0, u1, v1, tint); + } + private static void AppendQuad(List 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 /// Upload + draw accumulated rects + text. font may be null if only DrawRect was used. 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); diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs index 056ec01f..efefbf0b 100644 --- a/src/AcDream.App/Rendering/TextureCache.cs +++ b/src/AcDream.App/Rendering/TextureCache.cs @@ -14,6 +14,7 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab private readonly GL _gl; private readonly DatCollection _dats; private readonly Dictionary _handlesBySurfaceId = new(); + private readonly Dictionary _sizeBySurfaceId = new(); /// /// Composite cache for surface-with-override-origtex entries (Phase 5 /// TextureChanges). Key = (baseSurfaceId, overrideOrigTextureId), @@ -80,6 +81,28 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab return h; } + /// + /// Like but also returns the decoded + /// pixel dimensions. UI 9-slice geometry needs the source size to + /// compute slice UVs. Cached alongside the handle. + /// + 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; + } + /// /// Alpha-channel histogram for one decoded texture. Used to diagnose /// "why are clouds not transparent" — if cloud textures come out with diff --git a/src/AcDream.App/UI/UiRenderContext.cs b/src/AcDream.App/UI/UiRenderContext.cs index 51ce7b83..01d81277 100644 --- a/src/AcDream.App/UI/UiRenderContext.cs +++ b/src/AcDream.App/UI/UiRenderContext.cs @@ -53,6 +53,11 @@ public sealed class UiRenderContext 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); + 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) { var f = font ?? DefaultFont;