Do not throw away widget geometry during layout invalidation, in case the content rectangle stays the same

This commit is contained in:
Ritchie Frodomar 2024-07-07 22:59:05 -04:00
parent 982b0c0fe1
commit c9c3f4288b
14 changed files with 326 additions and 48 deletions

View file

@ -166,8 +166,12 @@ public sealed class GuiManager : IFontFamilyProvider
{
isRendering = true;
renderer.SetLayer(0);
foreach (Widget widget in topLevels)
widget.RenderInternal(renderer);
renderer.RenderBatches();
isRendering = false;
}

View file

@ -13,6 +13,7 @@ public interface IGuiContext
GraphicsDevice GraphicsDevice { get; }
void Render(VertexPositionColorTexture[] vertices, int[] indices, Texture2D? texture, LayoutRect? clipRect = null);
void Render(VertexBuffer vertices, IndexBuffer indices, int offset, int primitiveCount, Texture2D? texture, LayoutRect? clipRect = null);
IFontFamily GetFallbackFont();
}

View file

@ -68,6 +68,26 @@ public struct LayoutRect
return new LayoutRect(0, 0, 0, 0);
}
}
public static bool operator ==(LayoutRect left, LayoutRect right)
{
return (Math.Abs(left.Left - right.Left) < float.Epsilon) && (Math.Abs(left.Top - right.Top) < float.Epsilon) && (Math.Abs(left.Width - right.Width) < float.Epsilon) && (Math.Abs(left.Height - right.Height) < float.Epsilon);
}
public static bool operator !=(LayoutRect left, LayoutRect right)
{
return !(left == right);
}
public override bool Equals(object? obj)
{
return obj is LayoutRect right && this == right;
}
public override int GetHashCode()
{
return HashCode.Combine(Left, Top, Width, Height);
}
}
public enum ClippingMode

View file

@ -21,7 +21,7 @@ public class GeometryHelper : IFontStashRenderer2
this.guiRenderer = guiRenderer;
this.clipRect = clipRect;
whiteMesh = new GuiMeshBuilder(null, opacity, desaturate);
whiteMesh = new GuiMeshBuilder(null, guiRenderer.GetVertexCount(null), guiRenderer.Layer, opacity, desaturate);
}
public GuiMesh ExportMesh()
@ -47,7 +47,7 @@ public class GeometryHelper : IFontStashRenderer2
if (!meshes.TryGetValue(texture, out GuiMeshBuilder? builder))
{
builder = new GuiMeshBuilder(texture, opacity, desaturate);
builder = new GuiMeshBuilder(texture, guiRenderer.GetVertexCount(texture), guiRenderer.Layer, opacity, desaturate);
meshes.Add(texture, builder);
}

View file

@ -9,11 +9,21 @@ public struct GuiSubMesh
public readonly int[] Indices;
public readonly Texture2D? Texture;
private bool isNewMesh = true;
public bool IsNew => isNewMesh;
public GuiSubMesh(VertexPositionColorTexture[] vertices, int[] indices, Texture2D? texture)
{
Vertices = vertices;
Indices = indices;
this.Texture = texture;
isNewMesh = true;
}
public void MarkAsOld()
{
isNewMesh = false;
}
}

View file

@ -6,16 +6,20 @@ namespace AcidicGUI.Rendering;
public sealed class GuiMeshBuilder
{
private readonly List<VertexPositionColorTexture> vertices = new();
private readonly List<int> indices = new();
private readonly Texture2D? texture;
private readonly float opacity;
private readonly bool desaturate;
private readonly List<int> indices = new();
private readonly Texture2D? texture;
private readonly float opacity;
private readonly bool desaturate;
private readonly int baseVertex;
private readonly float layer;
public GuiMeshBuilder(Texture2D? texture, float opacity, bool desaturate)
public GuiMeshBuilder(Texture2D? texture, int baseVertex, int layer, float opacity, bool desaturate)
{
this.texture = texture;
this.opacity = opacity;
this.desaturate = desaturate;
//this.baseVertex = baseVertex;
//this.layer = layer * float.Epsilon;
}
public VertexPositionColorTexture this[int index]
@ -32,6 +36,7 @@ public sealed class GuiMeshBuilder
{
int index = vertices.Count;
vertex.Position.Z = layer;
vertex.Color.A = (byte)(vertex.Color.A * opacity);
if (desaturate)
@ -63,9 +68,9 @@ public sealed class GuiMeshBuilder
public void AddTriangle(int a, int b, int c)
{
indices.Add(a);
indices.Add(b);
indices.Add(c);
indices.Add(a + baseVertex);
indices.Add(b + baseVertex);
indices.Add(c + baseVertex);
}
public void AddQuad(int a, int b, int c, int d)

View file

@ -1,3 +1,4 @@
using System.Collections;
using System.Numerics;
using Microsoft.Xna.Framework.Graphics;
@ -5,20 +6,206 @@ namespace AcidicGUI.Rendering;
public sealed class GuiRenderer
{
private readonly IGuiContext context;
private GuiBatch? whiteBatch;
private readonly Dictionary<Texture2D, GuiBatch> batches = new();
private readonly IGuiContext context;
private int layer;
public GuiRenderer(IGuiContext context)
{
this.context = context;
}
public int Layer => layer;
public void SetLayer(int newLayer)
{
this.layer = newLayer;
}
public void PushLayer()
{
layer++;
}
public void PopLayer()
{
layer--;
}
public void RenderGuiMesh(GuiMesh mesh)
{
foreach (GuiSubMesh subMesh in mesh.SubMeshes)
{
context.Render(subMesh.Vertices, subMesh.Indices, subMesh.Texture, mesh.ClipRect);
SubmitSubMesh(subMesh);
}
}
public int GetVertexCount(Texture2D? texture)
{
if (texture == null)
{
if (whiteBatch==null)
whiteBatch = new GuiBatch(context, GraphicsDevice, null);
return whiteBatch.VertexCount;
}
if (!batches.TryGetValue(texture, out GuiBatch? batch))
{
batch = new GuiBatch(context, GraphicsDevice, texture);
batches.Add(texture, batch);
}
return batch.VertexCount;
}
private void SubmitSubMesh(GuiSubMesh subMesh)
{
if (subMesh.Texture != null)
{
if (!batches.TryGetValue(subMesh.Texture, out GuiBatch? batch))
{
batch = new GuiBatch(context, GraphicsDevice, subMesh.Texture);
batches.Add(subMesh.Texture, batch);
}
batch.Submit(subMesh);
return;
}
if (whiteBatch==null)
whiteBatch = new GuiBatch(context, GraphicsDevice, null);
whiteBatch.Submit(subMesh);
}
public void RenderBatches()
{
whiteBatch?.DrawBatch();
foreach (GuiBatch batch in batches.Values)
batch.DrawBatch();
}
public GraphicsDevice GraphicsDevice => context.GraphicsDevice;
}
public sealed class GrowingList<T>
{
private readonly List<T> list = new();
private int insertIndex = 0;
public IEnumerator<T> GetEnumerator()
{
return list.GetEnumerator();
}
public void AddRange(IEnumerable<T> items)
{
foreach (T item in items)
Add(item);
}
public void Add(T item)
{
if (insertIndex == list.Count)
list.Add(item);
else
list[insertIndex] = item;
insertIndex++;
}
public void Clear()
{
insertIndex = 0;
}
public int Count => insertIndex;
public T[] ToArray()
{
return list.ToArray().AsSpan().Slice(0, insertIndex).ToArray();
}
}
public sealed class GuiBatch
{
private readonly GraphicsDevice device;
private VertexBuffer? vertexBuffer;
private IndexBuffer? indexBuffer;
private readonly Texture2D? texture;
private readonly IGuiContext context;
private readonly GrowingList<VertexPositionColorTexture> cpuVertices = new();
private readonly GrowingList<int> cpuIndices = new();
private int vertexDirtiness = 0;
private int indexDirtiness = 0;
private bool dirty;
public GuiBatch(IGuiContext context, GraphicsDevice device, Texture2D? texture)
{
this.context = context;
this.device = device;
this.texture = texture;
}
public int VertexCount => cpuVertices.Count;
public void Submit(GuiSubMesh subMesh)
{
if (subMesh.IsNew)
{
subMesh.MarkAsOld();
if (!dirty)
{
vertexDirtiness = cpuVertices.Count;
indexDirtiness = cpuIndices.Count;
}
dirty = true;
}
cpuVertices.AddRange(subMesh.Vertices);
cpuIndices.AddRange(subMesh.Indices);
}
public void DrawBatch()
{
if (cpuIndices.Count == 0 || cpuVertices.Count == 0)
return;
if (vertexBuffer == null || vertexBuffer.VertexCount < cpuVertices.Count)
{
vertexBuffer?.Dispose();
dirty = true;
vertexDirtiness = 0;
vertexBuffer = new VertexBuffer(device, typeof(VertexPositionColorTexture), cpuVertices.Count, BufferUsage.None);
}
if (indexBuffer == null || indexBuffer.IndexCount < cpuIndices.Count)
{
indexBuffer?.Dispose();
dirty = true;
indexDirtiness = 0;
indexBuffer = new IndexBuffer(device, typeof(int), cpuIndices.Count, BufferUsage.None);
}
if (dirty)
{
var vertexData = cpuVertices.ToArray();
var indexData = cpuIndices.ToArray();
vertexBuffer.SetData(vertexData, vertexDirtiness, vertexData.Length - vertexDirtiness);
indexBuffer.SetData(indexData, indexDirtiness, indexData.Length - indexDirtiness);
dirty = false;
}
context.Render(vertexBuffer, indexBuffer, 0, cpuIndices.Count / 3, texture, null);
cpuVertices.Clear();
cpuIndices.Clear();
}
}

View file

@ -779,29 +779,29 @@ public class TextWidget : Widget
);
}
private struct MarkupData
private class MarkupData
{
public int Length;
public Color? ColorOverride;
public Color Highlight;
public FontInfo? FontOverride;
public int Length;
public Color? ColorOverride;
public Color Highlight;
public FontInfo? FontOverride;
public FontWeight? Weight;
public int? FontSize;
public bool Italic;
public bool Underline;
public bool Strikethrough;
public bool Selected;
public string? Link;
public int? FontSize;
public bool Italic;
public bool Underline;
public bool Strikethrough;
public bool Selected;
public string? Link;
}
private class TextElement
{
public string Text;
public Vector2 Position;
public Vector2? MeasuredSize;
public bool IsNewLine;
public int SourceStart;
public int SourceEnd;
public MarkupData MarkupData;
public string Text;
public Vector2 Position;
public Vector2? MeasuredSize;
public bool IsNewLine;
public int SourceStart;
public int SourceEnd;
public MarkupData MarkupData = new();
}
}

View file

@ -5,18 +5,19 @@ namespace AcidicGUI.Widgets;
public partial class Widget
{
private Widget? layoutRoot;
private bool layoutIsDirty = true;
private Widget? layoutRoot;
private bool layoutIsDirty = true;
private HorizontalAlignment horizontalAlignment;
private VerticalAlignment verticalAlignment;
private LayoutRect calculatedLayoutRect;
private Vector2? cachedContentSize;
private Padding padding;
private Padding margin;
private Vector2 minimumSize;
private Vector2 maximumSize;
private Vector2 previousAvailableSize;
private Visibility visibility;
private VerticalAlignment verticalAlignment;
private LayoutRect geometryRect;
private LayoutRect calculatedLayoutRect;
private Vector2? cachedContentSize;
private Padding padding;
private Padding margin;
private Vector2 minimumSize;
private Vector2 maximumSize;
private Vector2 previousAvailableSize;
private Visibility visibility;
public Visibility Visibility
{
@ -199,6 +200,12 @@ public partial class Widget
ArrangeChildren(context, calculatedLayoutRect - margin);
if (geometryRect != calculatedLayoutRect)
{
InvalidateGeometry();
geometryRect = calculatedLayoutRect;
}
CalculateClipRect();
layoutIsDirty = false;
@ -290,6 +297,5 @@ public partial class Widget
layoutIsDirty = true;
cachedContentSize = null;
cachedGeometry = null;
}
}

View file

@ -199,7 +199,10 @@ public abstract partial class Widget : IFontFamilyProvider
}
renderer.RenderGuiMesh(cachedGeometry.Value);
renderer.RenderBatches();
renderer.PushLayer();
foreach (Widget child in children)
{
child.RenderInternal(renderer);

View file

@ -154,6 +154,44 @@ public sealed class GuiService :
vertices.Length, indices, 0, indices.Length / 3);
}
public void Render(
VertexBuffer vertices,
IndexBuffer indices,
int offset,
int primitiveCount,
Texture2D? texture,
LayoutRect? clipRect = null
)
{
var device = Game.GraphicsDevice;
if (defaultEffect == null)
{
defaultEffect = new SpriteEffect(Game.GraphicsDevice);
}
if (white == null)
{
white = new Texture2D(Game.GraphicsDevice, 1, 1);
white.SetData(new Color[] { Color.White });
}
if (primitiveCount == 0)
return;
defaultEffect.Techniques[0].Passes[0].Apply();
device.Textures[0] = texture ?? white;
device.SamplerStates[0] = SamplerState.LinearClamp;
device.BlendState = blendState;
device.RasterizerState = GetRasterizerState(clipRect);
device.DepthStencilState = DepthStencilState.DepthRead;
device.ScissorRectangle = clipRect.GetValueOrDefault();
device.SetVertexBuffer(vertices);
device.Indices = indices;
device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, offset, primitiveCount);
}
private RasterizerState GetRasterizerState(LayoutRect? clipRect)
{
scissor ??= new RasterizerState()

View file

@ -1,5 +1,5 @@
/outputDir:bin/Debug/net8.0/
/intermediateDir:obj/Debug/net8.0/
/outputDir:bin/Release/net8.0/
/intermediateDir:obj/Release/net8.0/
/platform:DesktopGL
/config:
/profile:Reach

View file

@ -2,6 +2,7 @@
using System.Reactive.Subjects;
using System.Text;
using AcidicGUI;
using Microsoft.VisualBasic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Serilog;
@ -132,6 +133,7 @@ internal sealed class SociallyDistantGame :
timeData = Time.Initialize();
graphicsManager = new GraphicsDeviceManager(this);
graphicsManager.GraphicsProfile = GraphicsProfile.HiDef;
tabbedTools = new TabbedToolCollection(this);

View file

@ -30,6 +30,7 @@ public class GuiController : GameComponent,
private readonly FlexPanel mainPanel = new();
private readonly StatusBar statusBar;
private readonly OverlayWidget workArea = new();
private readonly OverlayWidget overlays = new();
private readonly Box mainBox = new();
private readonly FloatingWorkspace floatingWindowArea = new();
private readonly GuiService guiService;
@ -58,7 +59,8 @@ public class GuiController : GameComponent,
notificationManager = new NotificationManager(game.WorldManager);
guiService.GuiRoot.TopLevels.Add(mainPanel);
guiService.GuiRoot.TopLevels.Add(overlays);
mainPanel.ChildWidgets.Add(statusBar);
mainPanel.ChildWidgets.Add(workArea);
@ -127,7 +129,7 @@ public class GuiController : GameComponent,
private OverlayWorkspace CreateOverlayWorkspace()
{
var workspace = new OverlayWorkspace();
this.workArea.ChildWidgets.Add(workspace);
this.overlays.ChildWidgets.Add(workspace);
return workspace;
}
@ -172,7 +174,7 @@ public class GuiController : GameComponent,
systemSettings?.Dispose();
systemSettings = null;
workArea.ChildWidgets.Remove(overlay);
overlays.ChildWidgets.Remove(overlay);
}
}
}