using Lib.Utils; using T3.Core.Utils; namespace Lib.render.@_; [Guid("c5707b79-859b-4d53-92e0-cbed53aae648")] internal sealed class _RenderFontBuffer : Instance<_RenderFontBuffer> { [Output(Guid = "3D2F53A3-F1F0-489B-B20B-BADB09CDAEBE")] public readonly Slot Buffer = new(); [Output(Guid = "A0ECA9CE-35AA-497D-B5C9-CDE52A7C8D58")] public readonly Slot VertexCount = new(); public _RenderFontBuffer() { _fontResource = new Resource(Filepath, OnFileChanged); _fontResource.AddDependentSlots(Buffer); Buffer.UpdateAction += UpdateMesh; } private bool OnFileChanged(FileResource file, BmFontDescription currentValue, out BmFontDescription newValue, out string failureReason) { if (BmFontDescription.TryInitializeFromFile(file.AbsolutePath, out newValue)) { failureReason = null; return true; } failureReason = "Failed to load font from file"; return false; } // Cached state (hot path avoids work if none changed) private string _lastText = string.Empty; private float _lastSize, _lastSpacing, _lastLineHeight; private Vector2 _lastPosition; private Vector4 _lastColor; private int _lastHAlign, _lastVAlign; private BmFontDescription _lastFontRef; private bool _bufferDirty = true; private BufferLayout[] _bufferContent = Array.Empty(); private int _lastVertexCount; private static readonly int HorizontalAlignCount = Enum.GetValues(typeof(BmFontDescription.HorizontalAligns)).Length; private static readonly int VerticalAlignCount = Enum.GetValues(typeof(BmFontDescription.VerticalAligns)).Length; private void UpdateMesh(EvaluationContext context) { if (!_fontResource.TryGetValue(context, out var font)) return; var text = Text.GetValue(context); if (string.IsNullOrEmpty(text)) text = " "; // Inputs var rawSize = Size.GetValue(context); var spacing = Spacing.GetValue(context); var lineHeight = LineHeight.GetValue(context); var position = Position.GetValue(context); var color = Color.GetValue(context); var hAlignInt = HorizontalAlign.GetValue(context).Clamp(0, HorizontalAlignCount - 1); var vAlignInt = VerticalAlign.GetValue(context).Clamp(0, VerticalAlignCount - 1); // Change detection bool fontChanged = !ReferenceEquals(font, _lastFontRef); bool textChanged = !string.Equals(text, _lastText, StringComparison.Ordinal); bool formattingChanged = _lastSize != rawSize || _lastSpacing != spacing || _lastLineHeight != lineHeight || _lastPosition != position || _lastColor != color || _lastHAlign != hAlignInt || _lastVAlign != vAlignInt; if (fontChanged || textChanged || formattingChanged) { _lastFontRef = font; _lastText = text; _lastSize = rawSize; _lastSpacing = spacing; _lastLineHeight= lineHeight; _lastPosition = position; _lastColor = color; _lastHAlign = hAlignInt; _lastVAlign = vAlignInt; // Ensure capacity (grow only) if (_bufferContent.Length < text.Length) _bufferContent = new BufferLayout[text.Length]; // Rebuild CPU buffer _lastVertexCount = RebuildBuffer( font, text, rawSize, spacing, lineHeight, position, color, (BmFontDescription.HorizontalAligns)hAlignInt, (BmFontDescription.VerticalAligns)vAlignInt); _bufferDirty = true; } // Upload only when dirty or buffer missing if (_bufferDirty || Buffer.Value == null) { ResourceManager.SetupStructuredBuffer(_bufferContent, ref Buffer.Value); if (Buffer.Value != null) Buffer.Value.DebugName = nameof(_RenderFontBuffer); _bufferDirty = false; } VertexCount.Value = _lastVertexCount; } private int RebuildBuffer(BmFontDescription font, string text, float rawSize, float characterSpacing, float lineHeight, Vector2 position, Vector4 color, BmFontDescription.HorizontalAligns horizontalAlign, BmFontDescription.VerticalAligns verticalAlign) { // Line count without allocations int numLinesInText = 1; for (int i = 0; i < text.Length; i++) if (text[i] == '\n') numLinesInText++; float textureWidth = font.BmFont.Common.ScaleW; float textureHeight = font.BmFont.Common.ScaleH; // Scale to match 1080p 72DPI pt sizes (as in original) var scaleFactor = 1.0 / font.BmFont.Info.Size * 0.00185; var size = (float)(rawSize * scaleFactor); float cursorX = 0; float cursorY = 0; const float sdfWidth = 5f; // heuristic switch (verticalAlign) { case BmFontDescription.VerticalAligns.Top: cursorY = font.BmFont.Common.Base * (1 + sdfWidth / font.BmFont.Info.Size); break; case BmFontDescription.VerticalAligns.Middle: cursorY = font.BmFont.Common.LineHeight * lineHeight * (numLinesInText - 1) / 2 + font.BmFont.Common.LineHeight / 2f + font.BmFont.Common.Base * (sdfWidth / font.BmFont.Info.Size); break; case BmFontDescription.VerticalAligns.Bottom: cursorY = font.BmFont.Common.LineHeight * lineHeight * numLinesInText; break; } int outputIndex = 0; int currentLineCharacterCount = 0; int lineNumber = 0; int lastChar = 0; for (int idx = 0; idx < text.Length; idx++) { char c = text[idx]; if (c == '\n') { AdjustLineAlignment(horizontalAlign, cursorX, characterSpacing, size, currentLineCharacterCount, outputIndex); cursorY -= font.BmFont.Common.LineHeight * lineHeight; cursorX = 0; currentLineCharacterCount = 0; lastChar = 0; lineNumber++; continue; } if (!font.InfoForCharacter.TryGetValue(c, out var charInfo)) { lastChar = 0; continue; } if (lastChar != 0) { int key = lastChar | c; // kept as in original if (font.KerningForPairs.TryGetValue(key, out var kerning)) cursorX += kerning; } float sizeWidth = charInfo.Width * size; float sizeHeight = charInfo.Height * size; float x = position.X + (cursorX + charInfo.XOffset) * size; float y = position.Y + ((cursorY - charInfo.YOffset)) * size; if (charInfo.Width != 1 || charInfo.Height != 1) { _bufferContent[outputIndex] = new BufferLayout { Position = new Vector3(x, y, 0), CharHeight = sizeHeight, Orientation = Quaternion.Identity, AspectRatio = sizeWidth / sizeHeight, Color = color, UvMinMax = new Vector4( charInfo.X / textureWidth, charInfo.Y / textureHeight, (charInfo.X + charInfo.Width) / textureWidth, (charInfo.Y + charInfo.Height) / textureHeight), Id = (uint)outputIndex, LineNumber = (uint)lineNumber, Offset = new Vector2(charInfo.XOffset, charInfo.YOffset), }; outputIndex++; } currentLineCharacterCount++; cursorX += charInfo.XAdvance + characterSpacing; lastChar = c; } // Align last line AdjustLineAlignment(horizontalAlign, cursorX, characterSpacing, size, currentLineCharacterCount, outputIndex); // Return vertex count (6 per quad) return outputIndex * 6; } private void AdjustLineAlignment(BmFontDescription.HorizontalAligns horizontalAlign, float cursorX, float characterSpacing, float size, int currentLineCharacterCount, int outputIndex) { switch (horizontalAlign) { case BmFontDescription.HorizontalAligns.Center: OffsetLineCharacters((cursorX / 2 - characterSpacing / 2) * size, currentLineCharacterCount, outputIndex); break; case BmFontDescription.HorizontalAligns.Right: OffsetLineCharacters(cursorX * size, currentLineCharacterCount, outputIndex); break; } } private void OffsetLineCharacters(float offset, int currentLineCharacterCount, int outputIndex) { for (int backIndex = 0; backIndex <= currentLineCharacterCount; backIndex++) { int index0 = outputIndex - backIndex; if (index0 < 0 || index0 >= _bufferContent.Length) continue; _bufferContent[index0].Position.X -= offset; } } private Resource _fontResource; [StructLayout(LayoutKind.Explicit, Size = StructSize)] public struct BufferLayout { [FieldOffset(0)] public Vector3 Position; [FieldOffset(3 * 4)] public float CharHeight; [FieldOffset(4 * 4)] public float AspectRatio; [FieldOffset(5 * 4)] public Quaternion Orientation; [FieldOffset(9 * 4)] public Vector4 Color; [FieldOffset(13 * 4)] public Vector4 UvMinMax; [FieldOffset(17 * 4)] public uint Id; [FieldOffset(18 * 4)] public uint LineNumber; [FieldOffset(19 * 4)] public Vector2 Offset; private const int StructSize = 21 * 4; } // Inputs ---------------------------------------------------- [Input(Guid = "F2DD87B1-7F37-4B02-871B-B2E35972F246")] public readonly InputSlot Text = new(); [Input(Guid = "E827FDD1-20CA-473C-99EE-B839563690E9")] public readonly InputSlot Filepath = new(); [Input(Guid = "1CDE902D-5EAA-4144-B579-85F54717356B")] public readonly InputSlot Color = new(); [Input(Guid = "5008E9B4-083A-4494-8F7C-50FE5D80FC35")] public readonly InputSlot Size = new(); [Input(Guid = "E05E143E-8D4C-4DE7-8C9C-7FA7755009D3")] public readonly InputSlot Spacing = new(); [Input(Guid = "9EB4E13F-0FE3-4ED9-9DF1-814F075A05DA")] public readonly InputSlot LineHeight = new(); [Input(Guid = "C4F03392-FF7E-4B4A-8740-F93A581B2B6B")] public readonly InputSlot Position = new(); [Input(Guid = "14829EAC-BA59-4D31-90DC-53C7FC56CC30")] public readonly InputSlot VerticalAlign = new(); [Input(Guid = "E43BC887-D425-4F9C-8A86-A32C761DE0CC")] public readonly InputSlot HorizontalAlign = new(); }