mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Add Object Table Cache (#1708)
Proposed improvement to object table access speeds; prevents creating objects for every plugin iterating the object table. --------- Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com> Co-authored-by: Soreepeong <soreepeong@gmail.com>
This commit is contained in:
parent
6f2ebdc7a7
commit
31227016c1
3 changed files with 230 additions and 41 deletions
|
|
@ -1,13 +1,18 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Game.ClientState.Objects;
|
||||
|
|
@ -21,22 +26,45 @@ namespace Dalamud.Game.ClientState.Objects;
|
|||
#pragma warning disable SA1015
|
||||
[ResolveVia<IObjectTable>]
|
||||
#pragma warning restore SA1015
|
||||
internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
||||
internal sealed partial class ObjectTable : IServiceType, IObjectTable, IDisposable
|
||||
{
|
||||
private const int ObjectTableLength = 599;
|
||||
|
||||
private readonly ClientStateAddressResolver address;
|
||||
private readonly ClientState clientState;
|
||||
private readonly Framework framework;
|
||||
private readonly CachedEntry[] cachedObjectTable = new CachedEntry[ObjectTableLength];
|
||||
|
||||
private readonly ObjectPool<Enumerator> multiThreadedEnumerators =
|
||||
new DefaultObjectPoolProvider().Create<Enumerator>();
|
||||
|
||||
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[64];
|
||||
|
||||
private long nextMultithreadedUsageWarnTime;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ObjectTable(ClientState clientState)
|
||||
private ObjectTable(ClientState clientState, Framework framework)
|
||||
{
|
||||
this.address = clientState.AddressResolver;
|
||||
this.clientState = clientState;
|
||||
this.framework = framework;
|
||||
foreach (ref var e in this.cachedObjectTable.AsSpan())
|
||||
e = CachedEntry.CreateNew();
|
||||
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
|
||||
this.frameworkThreadEnumerators[i] = new(this, i);
|
||||
|
||||
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
|
||||
framework.BeforeUpdate += this.FrameworkOnBeforeUpdate;
|
||||
Log.Verbose($"Object table address 0x{this.clientState.AddressResolver.ObjectTable.ToInt64():X}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr Address => this.address.ObjectTable;
|
||||
public nint Address
|
||||
{
|
||||
get
|
||||
{
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
return this.clientState.AddressResolver.ObjectTable;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Length => ObjectTableLength;
|
||||
|
|
@ -46,14 +74,17 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
{
|
||||
get
|
||||
{
|
||||
var address = this.GetObjectAddress(index);
|
||||
return this.CreateObjectReference(address);
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
return index is >= ObjectTableLength or < 0 ? null : this.cachedObjectTable[index].ActiveObject;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameObject? SearchById(ulong objectId)
|
||||
{
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
if (objectId is GameObject.InvalidGameObjectId or 0)
|
||||
return null;
|
||||
|
||||
|
|
@ -70,23 +101,22 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe IntPtr GetObjectAddress(int index)
|
||||
public nint GetObjectAddress(int index)
|
||||
{
|
||||
if (index < 0 || index >= ObjectTableLength)
|
||||
return IntPtr.Zero;
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
return *(IntPtr*)(this.address.ObjectTable + (8 * index));
|
||||
return index is < 0 or >= ObjectTableLength ? nint.Zero : this.GetObjectAddressUnsafe(index);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe GameObject? CreateObjectReference(IntPtr address)
|
||||
public unsafe GameObject? CreateObjectReference(nint address)
|
||||
{
|
||||
var clientState = Service<ClientState>.GetNullable();
|
||||
_ = this.WarnMultithreadedUsage();
|
||||
|
||||
if (clientState == null || clientState.LocalContentId == 0)
|
||||
if (this.clientState.LocalContentId == 0)
|
||||
return null;
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == nint.Zero)
|
||||
return null;
|
||||
|
||||
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address;
|
||||
|
|
@ -104,6 +134,88 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
_ => new GameObject(address),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this.framework.BeforeUpdate -= this.FrameworkOnBeforeUpdate;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool WarnMultithreadedUsage()
|
||||
{
|
||||
if (ThreadSafety.IsMainThread)
|
||||
return false;
|
||||
|
||||
var n = Environment.TickCount64;
|
||||
if (this.nextMultithreadedUsageWarnTime < n)
|
||||
{
|
||||
this.nextMultithreadedUsageWarnTime = n + 30000;
|
||||
|
||||
Log.Warning(
|
||||
"{plugin} is accessing {objectTable} outside the main thread. This is deprecated.",
|
||||
Service<PluginManager>.Get().FindCallingPlugin()?.Name ?? "<unknown plugin>",
|
||||
nameof(ObjectTable));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void FrameworkOnBeforeUpdate(IFramework unused)
|
||||
{
|
||||
for (var i = 0; i < ObjectTableLength; i++)
|
||||
this.cachedObjectTable[i].Update(this.GetObjectAddressUnsafe(i));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private unsafe nint GetObjectAddressUnsafe(int index) =>
|
||||
*(nint*)(this.clientState.AddressResolver.ObjectTable + (8 * index));
|
||||
|
||||
private struct CachedEntry
|
||||
{
|
||||
public GameObject? ActiveObject;
|
||||
public PlayerCharacter PlayerCharacter;
|
||||
public BattleNpc BattleNpc;
|
||||
public Npc Npc;
|
||||
public EventObj EventObj;
|
||||
public GameObject GameObject;
|
||||
|
||||
public static CachedEntry CreateNew() =>
|
||||
new()
|
||||
{
|
||||
PlayerCharacter = new(nint.Zero),
|
||||
BattleNpc = new(nint.Zero),
|
||||
Npc = new(nint.Zero),
|
||||
EventObj = new(nint.Zero),
|
||||
GameObject = new(nint.Zero),
|
||||
};
|
||||
|
||||
public unsafe void Update(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
{
|
||||
this.ActiveObject = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address;
|
||||
var objKind = (ObjectKind)obj->ObjectKind;
|
||||
var activeObject = objKind switch
|
||||
{
|
||||
ObjectKind.Player => this.PlayerCharacter,
|
||||
ObjectKind.BattleNpc => this.BattleNpc,
|
||||
ObjectKind.EventNpc => this.Npc,
|
||||
ObjectKind.Retainer => this.Npc,
|
||||
ObjectKind.EventObj => this.EventObj,
|
||||
ObjectKind.Companion => this.Npc,
|
||||
ObjectKind.MountType => this.Npc,
|
||||
ObjectKind.Ornament => this.Npc,
|
||||
_ => this.GameObject,
|
||||
};
|
||||
activeObject.Address = address;
|
||||
this.ActiveObject = activeObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -117,17 +229,87 @@ internal sealed partial class ObjectTable
|
|||
/// <inheritdoc/>
|
||||
public IEnumerator<GameObject> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < ObjectTableLength; i++)
|
||||
if (this.WarnMultithreadedUsage())
|
||||
{
|
||||
var obj = this[i];
|
||||
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
yield return obj;
|
||||
// let's not
|
||||
var e = this.multiThreadedEnumerators.Get();
|
||||
e.InitializeForPooledObjects(this);
|
||||
return e;
|
||||
}
|
||||
|
||||
foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
|
||||
{
|
||||
if (x is not null)
|
||||
{
|
||||
var t = x;
|
||||
x = null;
|
||||
t.Reset();
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
return new Enumerator(this, -1);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private sealed class Enumerator : IEnumerator<GameObject>, IResettable
|
||||
{
|
||||
private readonly int slotId;
|
||||
private ObjectTable? owner;
|
||||
|
||||
private int index = -1;
|
||||
|
||||
public Enumerator() => this.slotId = -1;
|
||||
|
||||
public Enumerator(ObjectTable owner, int slotId)
|
||||
{
|
||||
this.owner = owner;
|
||||
this.slotId = slotId;
|
||||
}
|
||||
|
||||
public GameObject Current { get; private set; } = null!;
|
||||
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (this.index == ObjectTableLength)
|
||||
return false;
|
||||
|
||||
var cache = this.owner!.cachedObjectTable.AsSpan();
|
||||
for (this.index++; this.index < ObjectTableLength; this.index++)
|
||||
{
|
||||
if (cache[this.index].ActiveObject is { } ao)
|
||||
{
|
||||
this.Current = ao;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void InitializeForPooledObjects(ObjectTable ot) => this.owner = ot;
|
||||
|
||||
public void Reset() => this.index = -1;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.owner is not { } o)
|
||||
return;
|
||||
|
||||
if (this.index == -1)
|
||||
o.multiThreadedEnumerators.Return(this);
|
||||
else
|
||||
o.frameworkThreadEnumerators[this.slotId] = this;
|
||||
}
|
||||
|
||||
public bool TryReset()
|
||||
{
|
||||
this.Reset();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public unsafe partial class GameObject : IEquatable<GameObject>
|
|||
/// <summary>
|
||||
/// Gets the address of the game object in memory.
|
||||
/// </summary>
|
||||
public IntPtr Address { get; }
|
||||
public IntPtr Address { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Dalamud instance.
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ namespace Dalamud.Game;
|
|||
internal sealed class Framework : IDisposable, IServiceType, IFramework
|
||||
{
|
||||
private static readonly ModuleLog Log = new("Framework");
|
||||
|
||||
|
||||
private static readonly Stopwatch StatsStopwatch = new();
|
||||
|
||||
|
||||
private readonly GameLifecycle lifecycle;
|
||||
|
||||
private readonly Stopwatch updateStopwatch = new();
|
||||
|
|
@ -76,6 +76,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
/// <inheritdoc/>
|
||||
public event IFramework.OnUpdateDelegate? Update;
|
||||
|
||||
/// <summary>
|
||||
/// Executes during FrameworkUpdate before all <see cref="Update"/> delegates.
|
||||
/// </summary>
|
||||
internal event IFramework.OnUpdateDelegate BeforeUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the collection of stats is enabled.
|
||||
/// </summary>
|
||||
|
|
@ -280,7 +285,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
this.updateStopwatch.Reset();
|
||||
StatsStopwatch.Reset();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a update time to the stats history.
|
||||
/// </summary>
|
||||
|
|
@ -307,7 +312,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
internal void ProfileAndInvoke(IFramework.OnUpdateDelegate? eventDelegate, IFramework frameworkInstance)
|
||||
{
|
||||
if (eventDelegate is null) return;
|
||||
|
||||
|
||||
var invokeList = eventDelegate.GetInvocationList();
|
||||
|
||||
// Individually invoke OnUpdate handlers and time them.
|
||||
|
|
@ -353,6 +358,8 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
ThreadSafety.MarkMainThread();
|
||||
|
||||
this.BeforeUpdate?.InvokeSafely(this);
|
||||
|
||||
this.hitchDetector.Start();
|
||||
|
||||
try
|
||||
|
|
@ -425,7 +432,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
this.hitchDetector.Stop();
|
||||
|
||||
original:
|
||||
original:
|
||||
return this.updateHook.OriginalDisposeSafe(framework);
|
||||
}
|
||||
|
||||
|
|
@ -558,19 +565,19 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
|
|||
|
||||
/// <inheritdoc/>
|
||||
public DateTime LastUpdate => this.frameworkService.LastUpdate;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInFrameworkUpdateThread => this.frameworkService.IsInFrameworkUpdateThread;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -582,27 +589,27 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
|
|||
/// <inheritdoc/>
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<T> func)
|
||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RunOnFrameworkThread(Action action)
|
||||
=> this.frameworkService.RunOnFrameworkThread(action);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<Task<T>> func)
|
||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RunOnFrameworkThread(Func<Task> func)
|
||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnTick<T>(Func<T> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||
=> this.frameworkService.RunOnTick(action, delay, delayTicks, cancellationToken);
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnTick<T>(Func<Task<T>> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
|
||||
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue