Files
sbox-public/engine/Sandbox.Engine/Systems/Render/TextRendering/TextRendering.cs
Lorenz Junglas f0e763c414 Fix CommandList.DrawText evicting textures (again) (#4341)
We need to call MakeReady in the execute delegate so text stays marked used in cached CommandLists.
2026-03-20 16:14:40 +01:00

105 lines
3.0 KiB
C#

using Sandbox.UI;
using System.Collections.Concurrent;
namespace Sandbox;
[Expose]
public static partial class TextRendering
{
// this might seem like a weird way to expose this, but let me explain my logic
//
// Scope will contain a bunch of shit to let us add letter spacing and shadows and shit
// But then we'll have GetOrCreateTexture that lets you create a texture with multiple scopes
// so you can basically render rich text, with different styles in different sections.
//
// This will stop using GetOrCreateTexture eventually, and will replace all of its functionality.
//
// I think we can switch the built in UI label to use this stuff too, if we make a version that
// instead of looking in a cache, just returns a self managed TextBlock or something.
/// <summary>
/// Create a texture from the scope. The texture will either be a cached version or will be rendered immediately
/// </summary>
public static Texture GetOrCreateTexture( in Scope scope, Vector2 clip = default, TextFlag flag = TextFlag.LeftTop )
{
if ( Application.IsHeadless )
return Texture.Invalid;
if ( clip == default ) clip = 8096;
var tb = GetOrCreateTextBlock( scope, flag, clip );
tb.MakeReady();
return tb.Texture;
}
/// <summary>
/// Resolves or creates a fully initialized <see cref="TextBlock"/> for the given scope.
/// Safe to call from any thread. MakeReady() must still be called on the render thread before drawing.
/// </summary>
internal static TextBlock GetOrCreateTextBlock( in Scope scope, TextFlag flag, Vector2 clip )
{
if ( Application.IsHeadless ) return null;
var hc = new HashCode();
hc.Add( scope );
hc.Add( new Vector2( clip ) );
hc.Add( flag );
var hash = hc.ToHashCode();
if ( Dictionary.TryGetValue( hash, out var tb ) )
return tb;
// Build a fully initialized candidate before publishing.
// If another thread wins the race, their instance is returned and ours is discarded.
var candidate = new TextBlock();
candidate.Clip = clip;
candidate.Flags = flag;
candidate.Initialize( scope );
// GetOrAdd is race-safe: only one instance wins the slot.
// Set CacheKey on the winner so MakeReady can re-register if Tick() evicts it.
var winner = Dictionary.GetOrAdd( hash, candidate );
winner.CacheKey = hash;
return winner;
}
static ConcurrentDictionary<int, TextBlock> Dictionary = new();
static RealTimeSince _timeSinceCleanup;
/// <summary>
/// Free old, unused textblocks (and their textures)
/// </summary>
internal static void Tick()
{
Assert.False( Application.IsHeadless );
if ( _timeSinceCleanup < 0.5f ) return;
_timeSinceCleanup = 0;
int total = Dictionary.Count;
int deleted = 0;
foreach ( var item in Dictionary )
{
if ( item.Value.TimeSinceUsed < 1.5f ) continue;
item.Value.Dispose();
Dictionary.TryRemove( item );
deleted++;
}
//Log.Info( $"TextManager: {total} ({deleted} deleted)" );
}
internal static void Shutdown()
{
foreach ( var item in Dictionary )
{
item.Value.Dispose();
}
Dictionary.Clear();
}
}