fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -6,6 +6,7 @@ using AcDream.Core.Items;
|
|||
using AcDream.Core.Textures;
|
||||
using AcDream.Core.Spells;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -32,7 +33,7 @@ namespace AcDream.App.UI;
|
|||
/// </summary>
|
||||
public sealed class IconComposer
|
||||
{
|
||||
private readonly DatCollection _dats;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly TextureCache _cache;
|
||||
private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new();
|
||||
private readonly Dictionary<(uint, uint, uint), ComposedIcon> _dragByTuple = new();
|
||||
|
|
@ -59,7 +60,7 @@ public sealed class IconComposer
|
|||
private readonly Dictionary<uint, uint> _effectDidByIndex = new();
|
||||
private readonly Dictionary<uint, DecodedTexture> _effectTileByDid = new();
|
||||
|
||||
public IconComposer(DatCollection dats, TextureCache cache)
|
||||
public IconComposer(IDatReaderWriter dats, TextureCache cache)
|
||||
{
|
||||
_dats = dats;
|
||||
_cache = cache;
|
||||
|
|
@ -93,7 +94,7 @@ public sealed class IconComposer
|
|||
{
|
||||
if (_underlayResolveTried) return;
|
||||
_underlayResolveTried = true;
|
||||
uint masterDid = (uint)_dats.Portal.Header.MasterMapId; // = 0x25000000
|
||||
uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000
|
||||
if (masterDid == 0) return;
|
||||
if (!_dats.Portal.TryGet<EnumIDMap>(masterDid, out var master)) return;
|
||||
if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008
|
||||
|
|
@ -125,7 +126,7 @@ public sealed class IconComposer
|
|||
{
|
||||
if (_effectResolveTried) return;
|
||||
_effectResolveTried = true;
|
||||
uint masterDid = (uint)_dats.Portal.Header.MasterMapId; // = 0x25000000
|
||||
uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000
|
||||
if (masterDid == 0) return;
|
||||
if (!_dats.Portal.TryGet<EnumIDMap>(masterDid, out var master)) return;
|
||||
if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Player;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -186,7 +187,7 @@ public sealed class CharacterSheetProvider
|
|||
/// never silently swallowed — and leave raise costs unavailable (0).
|
||||
/// </summary>
|
||||
public static DatReaderWriter.DBObjs.ExperienceTable? LoadExperienceTable(
|
||||
DatCollection dats, Action<string>? log = null)
|
||||
IDatReaderWriter dats, Action<string>? log = null)
|
||||
{
|
||||
if (dats is null) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,17 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
|
||||
private ChatChannelKind _activeChannel = ChatChannelKind.Say;
|
||||
|
||||
// UiText polls LinesProvider while drawing and hit-testing. Keep the fully
|
||||
// formatted + wrapped transcript until either its source revision or the
|
||||
// metrics that determine wrapping change. This makes an idle chat window
|
||||
// allocation-free instead of snapshotting/formatting/wrapping every frame.
|
||||
private IReadOnlyList<UiText.Line> _cachedTranscriptLines = Array.Empty<UiText.Line>();
|
||||
private long _cachedTranscriptRevision = -1;
|
||||
private float _cachedTranscriptWrapWidth = float.NaN;
|
||||
private UiDatFont? _cachedTranscriptDatFont;
|
||||
private BitmapFont? _cachedTranscriptDebugFont;
|
||||
internal int TranscriptLayoutBuildCount { get; private set; }
|
||||
|
||||
// ── Channel knowledge (ported from old UiChannelMenu — gmMainChatUI::InitTalkFocusMenu @0x4cdc50) ──
|
||||
|
||||
private static readonly (string Label, ChatChannelKind? Channel)[] ChannelItems =
|
||||
|
|
@ -215,7 +226,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
c.Transcript.OneLine = false;
|
||||
c.Transcript.Selectable = true;
|
||||
c.Transcript.BackgroundColor = new Vector4(0f, 0f, 0f, 0.35f); // retail translucent transcript
|
||||
c.Transcript.LinesProvider = () => BuildLines(vm, c.Transcript, datFont, debugFont);
|
||||
c.Transcript.LinesProvider = () => c.GetTranscriptLines(vm);
|
||||
|
||||
// ── Input ────────────────────────────────────────────────────────
|
||||
// Editable/selectable/one-line semantics and state sprites came from the
|
||||
|
|
@ -415,20 +426,35 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
/// <see cref="UiText.Line"/> record format, applying retail-faithful
|
||||
/// per-<see cref="ChatKind"/> colors.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<UiText.Line> BuildLines(
|
||||
ChatVM vm, UiText view, UiDatFont? datFont, BitmapFont? debugFont)
|
||||
private IReadOnlyList<UiText.Line> GetTranscriptLines(ChatVM vm)
|
||||
{
|
||||
float maxW = Transcript.Width - 2f * Transcript.Padding;
|
||||
UiDatFont? datFont = Transcript.DatFont;
|
||||
BitmapFont? debugFont = Transcript.Font;
|
||||
long revision = vm.Revision;
|
||||
|
||||
if (_cachedTranscriptRevision == revision
|
||||
&& _cachedTranscriptWrapWidth.Equals(maxW)
|
||||
&& ReferenceEquals(_cachedTranscriptDatFont, datFont)
|
||||
&& ReferenceEquals(_cachedTranscriptDebugFont, debugFont))
|
||||
{
|
||||
return _cachedTranscriptLines;
|
||||
}
|
||||
|
||||
var detailed = vm.RecentLinesDetailed();
|
||||
if (detailed.Count == 0) return Array.Empty<UiText.Line>();
|
||||
if (detailed.Count == 0)
|
||||
{
|
||||
return StoreTranscriptLayout(
|
||||
Array.Empty<UiText.Line>(), revision, maxW, datFont, debugFont);
|
||||
}
|
||||
|
||||
// Word-wrap each message to the transcript's current pixel width (ports retail
|
||||
// GlyphList::Recalculate @0x473800 — break at word boundaries when the line would
|
||||
// exceed wrapWidth). Re-evaluated each frame so wrapping follows window resize.
|
||||
float maxW = view.Width - 2f * view.Padding;
|
||||
// exceed wrapWidth). The cache key re-evaluates it after window resize.
|
||||
Func<string, float> measure =
|
||||
datFont is { } df ? s => df.MeasureWidth(s)
|
||||
: debugFont is { } bf ? s => bf.MeasureWidth(s)
|
||||
: s => s.Length * 7f;
|
||||
: static s => s.Length * 7f;
|
||||
|
||||
var result = new List<UiText.Line>(detailed.Count);
|
||||
foreach (var d in detailed)
|
||||
|
|
@ -437,7 +463,23 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
foreach (var frag in WrapText(d.Text, maxW, measure))
|
||||
result.Add(new UiText.Line(frag, color));
|
||||
}
|
||||
return result;
|
||||
return StoreTranscriptLayout(result, revision, maxW, datFont, debugFont);
|
||||
}
|
||||
|
||||
private IReadOnlyList<UiText.Line> StoreTranscriptLayout(
|
||||
IReadOnlyList<UiText.Line> lines,
|
||||
long revision,
|
||||
float wrapWidth,
|
||||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont)
|
||||
{
|
||||
_cachedTranscriptRevision = revision;
|
||||
_cachedTranscriptWrapWidth = wrapWidth;
|
||||
_cachedTranscriptDatFont = datFont;
|
||||
_cachedTranscriptDebugFont = debugFont;
|
||||
_cachedTranscriptLines = lines;
|
||||
TranscriptLayoutBuildCount++;
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ public sealed class ComponentBookTemplateFactory
|
|||
/// so later inventory refreshes instantiate rows without reading DAT files.
|
||||
/// </summary>
|
||||
public static ComponentBookTemplateFactory? TryLoad(
|
||||
DatCollection dats,
|
||||
IDatReaderWriter dats,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
Func<uint, UiDatFont?>? resolveFont)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -14,10 +15,10 @@ namespace AcDream.App.UI.Layout;
|
|||
/// </remarks>
|
||||
public sealed class DatStringResolver
|
||||
{
|
||||
private readonly DatCollection _dats;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly Dictionary<uint, StringTable?> _tables = new();
|
||||
|
||||
public DatStringResolver(DatCollection dats)
|
||||
public DatStringResolver(IDatReaderWriter dats)
|
||||
=> _dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
|
||||
public string? Resolve(UiStringInfoValue info)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ public sealed class EffectRowTemplateFactory
|
|||
public float Height => _template.Height;
|
||||
|
||||
public static EffectRowTemplateFactory? TryLoad(
|
||||
DatCollection dats,
|
||||
IDatReaderWriter dats,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
Func<uint, UiDatFont?>? resolveFont)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ public static class ItemListCellTemplate
|
|||
/// Returns 0 when the layout/element/attribute/prototype/media is absent (the caller keeps
|
||||
/// the <see cref="UiItemSlot"/> default).
|
||||
/// </summary>
|
||||
public static uint ResolveEmptySprite(DatCollection dats, uint listLayoutId, uint listElementId)
|
||||
public static uint ResolveEmptySprite(IDatReaderWriter dats, uint listLayoutId, uint listElementId)
|
||||
{
|
||||
var listLd = dats.Get<LayoutDesc>(listLayoutId);
|
||||
if (listLd is null) return 0;
|
||||
|
|
@ -52,7 +53,7 @@ public static class ItemListCellTemplate
|
|||
/// attribute <c>0x1000000E</c>.
|
||||
/// </summary>
|
||||
public static uint ResolveEmptySprite(
|
||||
DatCollection dats,
|
||||
IDatReaderWriter dats,
|
||||
ElementInfo resolvedRoot,
|
||||
uint listElementId)
|
||||
{
|
||||
|
|
@ -71,7 +72,7 @@ public static class ItemListCellTemplate
|
|||
/// <see cref="CatalogLayoutId"/>. Paperdoll lists select distinct prototypes for their
|
||||
/// jewelry, weapon, clothing, and armor locations even though the lists share one base.
|
||||
/// </summary>
|
||||
public static uint ResolvePrototypeEmptySprite(DatCollection dats, uint prototypeElementId)
|
||||
public static uint ResolvePrototypeEmptySprite(IDatReaderWriter dats, uint prototypeElementId)
|
||||
{
|
||||
if (prototypeElementId == 0) return 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
|
|
@ -179,7 +180,7 @@ public static class LayoutImporter
|
|||
/// </summary>
|
||||
/// <param name="dats">The dat collection to read the LayoutDesc from.</param>
|
||||
/// <param name="layoutId">The LayoutDesc dat id to read.</param>
|
||||
public static ElementInfo? ImportInfos(DatCollection dats, uint layoutId)
|
||||
public static ElementInfo? ImportInfos(IDatReaderWriter dats, uint layoutId)
|
||||
{
|
||||
var ld = dats.Get<LayoutDesc>(layoutId);
|
||||
if (ld is null) return null;
|
||||
|
|
@ -234,7 +235,7 @@ public static class LayoutImporter
|
|||
/// top-level template. DialogFactory uses this path for the shared dialog catalog.
|
||||
/// </summary>
|
||||
public static ElementInfo? ImportInfos(
|
||||
DatCollection dats,
|
||||
IDatReaderWriter dats,
|
||||
uint layoutId,
|
||||
uint rootElementId)
|
||||
{
|
||||
|
|
@ -273,7 +274,7 @@ public static class LayoutImporter
|
|||
/// <param name="fontResolve">Optional per-element font resolver (see
|
||||
/// <see cref="Build"/> for details). Null = original single-font behavior.</param>
|
||||
public static ImportedLayout? Import(
|
||||
DatCollection dats,
|
||||
IDatReaderWriter dats,
|
||||
uint layoutId,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
UiDatFont? datFont,
|
||||
|
|
@ -287,7 +288,7 @@ public static class LayoutImporter
|
|||
|
||||
/// <summary>Import one selected root from a catalog-style LayoutDesc.</summary>
|
||||
public static ImportedLayout? Import(
|
||||
DatCollection dats,
|
||||
IDatReaderWriter dats,
|
||||
uint layoutId,
|
||||
uint rootElementId,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
|
|
@ -314,7 +315,7 @@ public static class LayoutImporter
|
|||
/// (cycle-guarded by <paramref name="baseChain"/>), then resolves + attaches children.
|
||||
/// </summary>
|
||||
private static ElementInfo Resolve(
|
||||
DatCollection dats,
|
||||
IDatReaderWriter dats,
|
||||
ElementDesc d,
|
||||
HashSet<(uint layoutId, uint elementId)> baseChain)
|
||||
{
|
||||
|
|
@ -369,7 +370,7 @@ public static class LayoutImporter
|
|||
}
|
||||
|
||||
private static void IncorporateChildren(
|
||||
DatCollection dats,
|
||||
IDatReaderWriter dats,
|
||||
ElementInfo result,
|
||||
IReadOnlyList<ElementInfo>? baseChildren,
|
||||
ElementDesc derived)
|
||||
|
|
@ -406,7 +407,7 @@ public static class LayoutImporter
|
|||
}
|
||||
|
||||
private static ElementInfo IncorporateResolvedChild(
|
||||
DatCollection dats,
|
||||
IDatReaderWriter dats,
|
||||
ElementInfo baseChild,
|
||||
ElementDesc derivedChild)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Textures;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
|
|
@ -38,11 +39,11 @@ public sealed class PaperdollClickMap
|
|||
/// and decodes the resulting RenderSurface for exact per-pixel sampling.
|
||||
/// The caller owns synchronization around <paramref name="dats"/>.
|
||||
/// </summary>
|
||||
public static PaperdollClickMap? Load(DatCollection dats)
|
||||
public static PaperdollClickMap? Load(IDatReaderWriter dats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
|
||||
uint masterDid = (uint)dats.Portal.Header.MasterMapId;
|
||||
uint masterDid = (uint)dats.Portal.Db.Header.MasterMapId;
|
||||
if (masterDid == 0
|
||||
|| !dats.Portal.TryGet<EnumIDMap>(masterDid, out var master)
|
||||
|| master is null
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -56,7 +57,7 @@ public static class PaperdollSlotBackgrounds
|
|||
};
|
||||
|
||||
/// <summary>Resolves every supported slot's exact empty RenderSurface from the live DAT.</summary>
|
||||
public static IReadOnlyDictionary<uint, uint> ResolveEmptySprites(DatCollection dats)
|
||||
public static IReadOnlyDictionary<uint, uint> ResolveEmptySprites(IDatReaderWriter dats)
|
||||
{
|
||||
var result = new Dictionary<uint, uint>(Definitions.Length);
|
||||
foreach (Definition definition in Definitions)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ public sealed class RadarSnapshotProvider
|
|||
private readonly Func<bool> _coordinatesOnRadar;
|
||||
private readonly Func<bool> _uiLocked;
|
||||
private readonly Func<uint, RadarRelationshipTraits>? _relationshipFor;
|
||||
private readonly Action<uint, int, List<KeyValuePair<uint, WorldEntity>>>? _copySpatialCandidates;
|
||||
private readonly List<KeyValuePair<uint, WorldEntity>> _candidateScratch = new();
|
||||
|
||||
public RadarSnapshotProvider(
|
||||
ClientObjectTable objects,
|
||||
|
|
@ -41,7 +43,8 @@ public sealed class RadarSnapshotProvider
|
|||
Func<bool> coordinatesOnRadar,
|
||||
Func<bool> uiLocked,
|
||||
Func<uint, RadarRelationshipTraits>? relationshipFor = null,
|
||||
Func<IReadOnlyDictionary<uint, WorldEntity>>? playerEntities = null)
|
||||
Func<IReadOnlyDictionary<uint, WorldEntity>>? playerEntities = null,
|
||||
Action<uint, int, List<KeyValuePair<uint, WorldEntity>>>? copySpatialCandidates = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_worldEntities = worldEntities ?? throw new ArgumentNullException(nameof(worldEntities));
|
||||
|
|
@ -54,6 +57,7 @@ public sealed class RadarSnapshotProvider
|
|||
_coordinatesOnRadar = coordinatesOnRadar ?? throw new ArgumentNullException(nameof(coordinatesOnRadar));
|
||||
_uiLocked = uiLocked ?? throw new ArgumentNullException(nameof(uiLocked));
|
||||
_relationshipFor = relationshipFor;
|
||||
_copySpatialCandidates = copySpatialCandidates;
|
||||
}
|
||||
|
||||
public UiRadarSnapshot BuildSnapshot()
|
||||
|
|
@ -85,14 +89,33 @@ public sealed class RadarSnapshotProvider
|
|||
(uint)(_objects.Get(playerGuid)?.Type ?? ItemType.None), playerPwd);
|
||||
float range = RetailRadar.GetRangeMeters(isOutside);
|
||||
|
||||
var blips = new List<UiRadarBlip>(worldEntities.Count);
|
||||
foreach (var pair in worldEntities)
|
||||
_candidateScratch.Clear();
|
||||
if (_copySpatialCandidates is not null)
|
||||
{
|
||||
// Retail radar is at most 75 metres while a landblock is 192
|
||||
// metres wide. The current block plus its eight neighbours is a
|
||||
// complete broadphase (the cheap spatial pre-filter).
|
||||
_copySpatialCandidates(playerCellId, 1, _candidateScratch);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var pair in worldEntities)
|
||||
_candidateScratch.Add(pair);
|
||||
}
|
||||
|
||||
var blips = new List<UiRadarBlip>(Math.Min(_candidateScratch.Count, 64));
|
||||
foreach (var pair in _candidateScratch)
|
||||
{
|
||||
uint guid = pair.Key;
|
||||
if (guid == playerGuid)
|
||||
continue;
|
||||
|
||||
var entity = pair.Value;
|
||||
if (!worldEntities.TryGetValue(guid, out WorldEntity? visibleEntity)
|
||||
|| !ReferenceEquals(entity, visibleEntity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var clientObject = _objects.Get(guid);
|
||||
spawns.TryGetValue(guid, out var spawn);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -33,7 +34,7 @@ public readonly record struct SpellbookRowStyle(
|
|||
/// Returns null if the authored prototype is incomplete; callers then leave the
|
||||
/// panel unbound rather than inventing replacement art.
|
||||
/// </summary>
|
||||
public static SpellbookRowStyle? TryLoad(DatCollection dats)
|
||||
public static SpellbookRowStyle? TryLoad(IDatReaderWriter dats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ElementInfo? prototype = LayoutImporter.ImportInfos(dats, CatalogLayoutId, PrototypeId);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -9,11 +10,11 @@ namespace AcDream.App.UI;
|
|||
/// </summary>
|
||||
public static class RetailDataIdResolver
|
||||
{
|
||||
public static uint Resolve(DatCollection dats, uint enumValue, uint enumCategory)
|
||||
public static uint Resolve(IDatReaderWriter dats, uint enumValue, uint enumCategory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
|
||||
uint masterDid = (uint)dats.Portal.Header.MasterMapId;
|
||||
uint masterDid = (uint)dats.Portal.Db.Header.MasterMapId;
|
||||
if (masterDid == 0
|
||||
|| !dats.Portal.TryGet<EnumIDMap>(masterDid, out var master)
|
||||
|| master is null
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using AcDream.Core.Net;
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Content;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
|
@ -22,7 +23,7 @@ using Silk.NET.Input;
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
public sealed record RetailUiAssets(
|
||||
DatCollection Dats,
|
||||
IDatReaderWriter Dats,
|
||||
object DatLock,
|
||||
Func<uint, (uint Texture, int Width, int Height)> ResolveSprite,
|
||||
Func<uint, UiDatFont?> ResolveFont,
|
||||
|
|
@ -186,14 +187,15 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
{
|
||||
private readonly RetailUiRuntimeBindings _bindings;
|
||||
private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity;
|
||||
private readonly RetailWindowLayoutPersistence? _persistence;
|
||||
private readonly RetailUiAutomationScriptRunner? _automation;
|
||||
private RetailWindowLayoutPersistence? _persistence;
|
||||
private RetailUiAutomationScriptRunner? _automation;
|
||||
private readonly RetailPanelUiController _panelUi;
|
||||
private GameplayConfirmationController? _gameplayConfirmationController;
|
||||
private RetailItemConfirmationController? _itemConfirmationController;
|
||||
private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController;
|
||||
private UiShortcutDigitGraphics? _shortcutDigitGraphics;
|
||||
private VividTargetIndicatorController? _vividTargetIndicator;
|
||||
private ResourceShutdownTransaction? _shutdown;
|
||||
private bool _disposed;
|
||||
|
||||
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
||||
|
|
@ -203,6 +205,11 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
bindings.Host.IsWindowVisible,
|
||||
bindings.Host.ShowWindow,
|
||||
bindings.Host.HideWindow);
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
RetailUiRuntimeBindings bindings = _bindings;
|
||||
MountFpsDisplay();
|
||||
MountVividTargetIndicator();
|
||||
MountVitals();
|
||||
|
|
@ -284,13 +291,26 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bindings);
|
||||
var runtime = new RetailUiRuntime(bindings);
|
||||
try
|
||||
{
|
||||
return new RetailUiRuntime(bindings);
|
||||
runtime.Initialize();
|
||||
return runtime;
|
||||
}
|
||||
catch
|
||||
catch (Exception initializationFailure)
|
||||
{
|
||||
bindings.Host.Dispose();
|
||||
try
|
||||
{
|
||||
runtime.Dispose();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Retail UI initialization failed and its partially constructed ownership could not be fully released.",
|
||||
initializationFailure,
|
||||
cleanupFailure);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
|
@ -1699,14 +1719,57 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_persistence?.Dispose();
|
||||
Host.WindowManager.WindowVisibilityChanged -= OnWindowVisibilityChanged;
|
||||
_itemConfirmationController?.Dispose();
|
||||
_gameplayConfirmationController?.Dispose();
|
||||
DialogFactory?.Dispose();
|
||||
_panelUi.Dispose();
|
||||
Host.Dispose();
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_shutdown ??= CreateShutdownTransaction(
|
||||
() => _persistence?.Dispose(),
|
||||
() => Host.WindowManager.WindowVisibilityChanged -= OnWindowVisibilityChanged,
|
||||
() => _itemConfirmationController?.Dispose(),
|
||||
() => _gameplayConfirmationController?.Dispose(),
|
||||
() => DialogFactory?.Dispose(),
|
||||
_panelUi.Dispose,
|
||||
Host.Dispose);
|
||||
_shutdown.CompleteOrThrow();
|
||||
_disposed = _shutdown.IsComplete;
|
||||
}
|
||||
|
||||
internal static ResourceShutdownTransaction CreateShutdownTransaction(
|
||||
Action disposePersistence,
|
||||
Action unsubscribeWindowVisibility,
|
||||
Action disposeItemConfirmation,
|
||||
Action disposeGameplayConfirmation,
|
||||
Action disposeDialogFactory,
|
||||
Action disposePanelController,
|
||||
Action disposeHost)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(disposePersistence);
|
||||
ArgumentNullException.ThrowIfNull(unsubscribeWindowVisibility);
|
||||
ArgumentNullException.ThrowIfNull(disposeItemConfirmation);
|
||||
ArgumentNullException.ThrowIfNull(disposeGameplayConfirmation);
|
||||
ArgumentNullException.ThrowIfNull(disposeDialogFactory);
|
||||
ArgumentNullException.ThrowIfNull(disposePanelController);
|
||||
ArgumentNullException.ThrowIfNull(disposeHost);
|
||||
|
||||
return new ResourceShutdownTransaction(
|
||||
new ResourceShutdownStage("retail UI observers",
|
||||
[
|
||||
new("window persistence", disposePersistence),
|
||||
new("window visibility", unsubscribeWindowVisibility),
|
||||
]),
|
||||
new ResourceShutdownStage("retail UI semantic controllers",
|
||||
[
|
||||
new("item confirmation", disposeItemConfirmation),
|
||||
new("gameplay confirmation", disposeGameplayConfirmation),
|
||||
]),
|
||||
new ResourceShutdownStage("retail UI panel composition",
|
||||
[
|
||||
new("dialog factory", disposeDialogFactory),
|
||||
new("panel controller", disposePanelController),
|
||||
]),
|
||||
new ResourceShutdownStage("retained UI host",
|
||||
[
|
||||
new("host", disposeHost),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
|
@ -89,7 +90,7 @@ public sealed class UiDatFont
|
|||
/// path the D.2b chrome sprites use). Returns null if the Font DBObj is
|
||||
/// missing — callers fall back to the debug bitmap font.
|
||||
/// </summary>
|
||||
public static UiDatFont? Load(DatCollection dats, TextureCache cache, uint fontId = DefaultFontId)
|
||||
public static UiDatFont? Load(IDatReaderWriter dats, TextureCache cache, uint fontId = DefaultFontId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(cache);
|
||||
|
|
|
|||
|
|
@ -171,8 +171,19 @@ public abstract class UiElement
|
|||
/// </summary>
|
||||
public bool IsEditControl { get; set; }
|
||||
|
||||
private int _zOrder;
|
||||
|
||||
/// <summary>Painter's-algorithm z-order within siblings. Higher = on top.</summary>
|
||||
public int ZOrder { get; set; }
|
||||
public int ZOrder
|
||||
{
|
||||
get => _zOrder;
|
||||
set
|
||||
{
|
||||
if (_zOrder == value) return;
|
||||
_zOrder = value;
|
||||
Parent?.InvalidateChildOrder();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Window opacity (0..1) multiplied into this element's and its
|
||||
/// descendants' background + sprite draws (text stays opaque). 1 = fully opaque.
|
||||
|
|
@ -269,6 +280,8 @@ public abstract class UiElement
|
|||
public UiElement? Parent { get; private set; }
|
||||
|
||||
private readonly List<UiElement> _children = new();
|
||||
private UiElement[]? _childrenBackToFront;
|
||||
private UiElement[]? _childrenFrontToBack;
|
||||
public IReadOnlyList<UiElement> Children => _children;
|
||||
|
||||
public virtual void AddChild(UiElement child)
|
||||
|
|
@ -276,6 +289,7 @@ public abstract class UiElement
|
|||
if (child.Parent is not null) child.Parent.RemoveChild(child);
|
||||
child.Parent = this;
|
||||
_children.Add(child);
|
||||
InvalidateChildOrder();
|
||||
}
|
||||
|
||||
public virtual bool RemoveChild(UiElement child)
|
||||
|
|
@ -284,9 +298,52 @@ public abstract class UiElement
|
|||
FindRoot()?.OnSubtreeRemoving(child);
|
||||
_children.Remove(child);
|
||||
child.Parent = null;
|
||||
InvalidateChildOrder();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stable snapshots of the current sibling order. Retained UI mutation is
|
||||
/// render-thread-owned, so the arrays can be reused until membership or a
|
||||
/// child's Z-order changes. A traversal keeps its local array if a callback
|
||||
/// mutates the tree, preserving the prior snapshot semantics without a
|
||||
/// per-element allocation on every draw and overlay pass.
|
||||
/// </summary>
|
||||
internal UiElement[] ChildrenBackToFrontSnapshot()
|
||||
{
|
||||
if (_childrenBackToFront is not null)
|
||||
return _childrenBackToFront;
|
||||
|
||||
_childrenBackToFront = _children.ToArray();
|
||||
Array.Sort(
|
||||
_childrenBackToFront,
|
||||
static (a, b) => a.ZOrder.CompareTo(b.ZOrder));
|
||||
return _childrenBackToFront;
|
||||
}
|
||||
|
||||
internal UiElement[] ChildrenFrontToBackSnapshot()
|
||||
{
|
||||
if (_childrenFrontToBack is not null)
|
||||
return _childrenFrontToBack;
|
||||
|
||||
UiElement[] backToFront = ChildrenBackToFrontSnapshot();
|
||||
_childrenFrontToBack = new UiElement[backToFront.Length];
|
||||
for (int source = backToFront.Length - 1, destination = 0;
|
||||
source >= 0;
|
||||
source--, destination++)
|
||||
{
|
||||
_childrenFrontToBack[destination] = backToFront[source];
|
||||
}
|
||||
|
||||
return _childrenFrontToBack;
|
||||
}
|
||||
|
||||
private void InvalidateChildOrder()
|
||||
{
|
||||
_childrenBackToFront = null;
|
||||
_childrenFrontToBack = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if this widget draws its full appearance itself and REPRODUCES its dat
|
||||
/// sub-elements procedurally (3-slice caps, button labels, scroll arrows, popup
|
||||
|
|
@ -405,9 +462,7 @@ public abstract class UiElement
|
|||
ctx.PushClip(0f, 0f, Width, Height);
|
||||
try
|
||||
{
|
||||
// Avoid LINQ allocation by copying to a temp array and sorting.
|
||||
var ordered = _children.ToArray();
|
||||
Array.Sort(ordered, static (a, b) => a.ZOrder.CompareTo(b.ZOrder));
|
||||
UiElement[] ordered = ChildrenBackToFrontSnapshot();
|
||||
for (int i = 0; i < ordered.Length; i++)
|
||||
ordered[i].DrawSelfAndChildren(ctx);
|
||||
}
|
||||
|
|
@ -449,8 +504,7 @@ public abstract class UiElement
|
|||
ctx.PushClip(0f, 0f, Width, Height);
|
||||
try
|
||||
{
|
||||
var ordered = _children.ToArray();
|
||||
Array.Sort(ordered, static (a, b) => a.ZOrder.CompareTo(b.ZOrder));
|
||||
UiElement[] ordered = ChildrenBackToFrontSnapshot();
|
||||
for (int i = 0; i < ordered.Length; i++)
|
||||
ordered[i].DrawOverlays(ctx);
|
||||
}
|
||||
|
|
@ -496,8 +550,7 @@ public abstract class UiElement
|
|||
// only whether THIS element claims the hit.
|
||||
if (_children.Count > 0)
|
||||
{
|
||||
var ordered = _children.ToArray();
|
||||
Array.Sort(ordered, static (a, b) => b.ZOrder.CompareTo(a.ZOrder));
|
||||
UiElement[] ordered = ChildrenFrontToBackSnapshot();
|
||||
for (int i = 0; i < ordered.Length; i++)
|
||||
{
|
||||
var c = ordered[i];
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ public sealed class UiHost : System.IDisposable
|
|||
|
||||
private long _startTicks = System.Environment.TickCount64;
|
||||
private readonly List<System.Action> _inputUnsubscribers = new();
|
||||
private ResourceShutdownTransaction? _shutdown;
|
||||
private bool _disposeRequested;
|
||||
private bool _disposed;
|
||||
|
||||
public UiHost(GL gl, string shaderDir, BitmapFont? defaultFont = null)
|
||||
|
|
@ -81,7 +83,7 @@ public sealed class UiHost : System.IDisposable
|
|||
|
||||
public void WireMouse(IMouse mouse)
|
||||
{
|
||||
System.ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
System.ObjectDisposedException.ThrowIf(_disposeRequested || _disposed, this);
|
||||
System.ArgumentNullException.ThrowIfNull(mouse);
|
||||
|
||||
void OnMouseDown(IMouse sender, MouseButton button) =>
|
||||
|
|
@ -107,7 +109,7 @@ public sealed class UiHost : System.IDisposable
|
|||
|
||||
public void WireKeyboard(IKeyboard kb)
|
||||
{
|
||||
System.ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
System.ObjectDisposedException.ThrowIf(_disposeRequested || _disposed, this);
|
||||
System.ArgumentNullException.ThrowIfNull(kb);
|
||||
Keyboard = kb; // last wired keyboard wins (one-keyboard desktop)
|
||||
void OnKeyDown(IKeyboard sender, Key key, int scanCode) => Root.OnKeyDown((int)key);
|
||||
|
|
@ -162,13 +164,55 @@ public sealed class UiHost : System.IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
for (int i = _inputUnsubscribers.Count - 1; i >= 0; i--)
|
||||
_inputUnsubscribers[i]();
|
||||
_inputUnsubscribers.Clear();
|
||||
Keyboard = null;
|
||||
WindowManager.Dispose();
|
||||
TextRenderer.Dispose();
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposeRequested = true;
|
||||
_shutdown ??= CreateShutdownTransaction(
|
||||
_inputUnsubscribers.AsEnumerable().Reverse().ToArray(),
|
||||
() =>
|
||||
{
|
||||
_inputUnsubscribers.Clear();
|
||||
Keyboard = null;
|
||||
},
|
||||
WindowManager.Dispose,
|
||||
TextRenderer.Dispose);
|
||||
_shutdown.CompleteOrThrow();
|
||||
_disposed = _shutdown.IsComplete;
|
||||
}
|
||||
|
||||
internal static ResourceShutdownTransaction CreateShutdownTransaction(
|
||||
IReadOnlyList<Action> inputUnsubscribers,
|
||||
Action releaseInputState,
|
||||
Action disposeWindowManager,
|
||||
Action disposeTextRenderer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inputUnsubscribers);
|
||||
ArgumentNullException.ThrowIfNull(releaseInputState);
|
||||
ArgumentNullException.ThrowIfNull(disposeWindowManager);
|
||||
ArgumentNullException.ThrowIfNull(disposeTextRenderer);
|
||||
|
||||
return new ResourceShutdownTransaction(
|
||||
new ResourceShutdownStage(
|
||||
"retained UI input subscriptions",
|
||||
inputUnsubscribers
|
||||
.Select((unsubscribe, index) => new ResourceShutdownOperation(
|
||||
$"input subscription {index}",
|
||||
unsubscribe ?? throw new ArgumentException(
|
||||
"Input unsubscriber entries cannot be null.",
|
||||
nameof(inputUnsubscribers))))
|
||||
.ToArray()),
|
||||
new ResourceShutdownStage("retained UI input state",
|
||||
[
|
||||
new("input state", releaseInputState),
|
||||
]),
|
||||
new ResourceShutdownStage("retained UI windows",
|
||||
[
|
||||
new("window manager", disposeWindowManager),
|
||||
]),
|
||||
new ResourceShutdownStage("retained UI renderer",
|
||||
[
|
||||
new("text renderer", disposeTextRenderer),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ public sealed class UiRoot : UiElement
|
|||
// A listener may synchronously close/remove a window. Snapshot the walk,
|
||||
// then skip children no longer owned by this parent so a deleted subtree
|
||||
// cannot receive a stale pulse and collection mutation cannot invalidate it.
|
||||
foreach (var child in element.Children.ToArray())
|
||||
foreach (var child in element.ChildrenBackToFrontSnapshot())
|
||||
if (ReferenceEquals(child.Parent, element))
|
||||
BroadcastGlobalUiTime(child, nowSeconds);
|
||||
}
|
||||
|
|
@ -898,10 +898,7 @@ public sealed class UiRoot : UiElement
|
|||
}
|
||||
|
||||
// Walk top-level children in reverse Z-order (topmost first).
|
||||
var kids = new UiElement[Children.Count];
|
||||
for (int i = 0; i < Children.Count; i++) kids[i] = Children[i];
|
||||
Array.Sort(kids, static (a, b) => b.ZOrder.CompareTo(a.ZOrder));
|
||||
foreach (var c in kids)
|
||||
foreach (var c in ChildrenFrontToBackSnapshot())
|
||||
{
|
||||
var cp = c.ScreenPosition;
|
||||
var hit = c.HitTest(x - cp.X, y - cp.Y);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public sealed class UiScrollablePanel : UiPanel
|
|||
|
||||
public void ClearContent()
|
||||
{
|
||||
foreach (var child in Children.ToArray())
|
||||
foreach (var child in ChildrenBackToFrontSnapshot())
|
||||
RemoveChild(child);
|
||||
_baseTops.Clear();
|
||||
ContentHeight = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue