Skip to content

Commit 2c50b53

Browse files
committed
implement a fast enough data component.
1 parent 714732c commit 2c50b53

3 files changed

Lines changed: 266 additions & 1 deletion

File tree

Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ internal static void LoadAuto() {
211211
Logger.Verbose("loader", $"ALL MODS LOADED IN {watch.ElapsedMilliseconds}ms");
212212
Logger.Info("loader", $"Loaded {Everest._Modules.Count} modules");
213213

214+
DataComponentRegistryBase.Optimize();
215+
214216
try {
215217
Watcher = new FileSystemWatcher {
216218
Path = PathMods,
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
using Celeste.Mod.Registry;
2+
using Monocle;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
using static Celeste.Mod.Registry.DataComponentRegistryBase;
9+
#nullable enable
10+
11+
namespace Celeste.Mod.Registry {
12+
public class DataComponentInfo {
13+
public string? ModName;
14+
public string? Description;
15+
}
16+
internal struct DebugModeDataComponentInfo {
17+
internal DataComponentInfo? info;
18+
internal bool unloaded;
19+
internal int knownUnloaded;
20+
}
21+
22+
internal static class DataComponentRegistryBase {
23+
internal abstract class SlotHolderBase {
24+
internal int slot = -1;
25+
internal int knownCount;
26+
internal abstract Type declaringType { get; }
27+
internal abstract Type fieldType { get; }
28+
internal static void Throw() {
29+
throw new InvalidOperationException("It's not prepared.");
30+
}
31+
}
32+
internal static int getHierarchyDepth(Type? self) {
33+
int depth = 0;
34+
while (self is not null) {
35+
self = self.BaseType;
36+
depth++;
37+
}
38+
return depth - 1;
39+
}
40+
internal static readonly Dictionary<Type, List<DebugModeDataComponentInfo>> debugInfos = new();
41+
42+
internal static readonly Dictionary<Type, List<DataComponentInfo?>> infos = new();
43+
internal static Dictionary<Type, List<SlotHolderBase>>? toOptimize = new();
44+
45+
internal static void Optimize() {
46+
if (toOptimize is not { } toop) {
47+
throw new InvalidOperationException("what did it mean");
48+
}
49+
toOptimize = null;
50+
Dictionary<Type, int> types = new();
51+
52+
foreach ((Type k, List<DataComponentInfo?> o) in infos) {
53+
static int GetOrSet(Dictionary<Type, int> types, Type t, int? hint) {
54+
if (types.TryGetValue(t, out int cur)) {
55+
return cur;
56+
}
57+
int baseCnt;
58+
if (t == typeof(Entity)) {
59+
baseCnt = 0;
60+
} else {
61+
baseCnt = GetOrSet(types, t.BaseType!, null);
62+
}
63+
return types[t] = baseCnt + (hint ?? infos.GetValueOrDefault(t)?.Count ?? 0);
64+
}
65+
GetOrSet(types, k, o.Count);
66+
}
67+
foreach ((Type? k, List<SlotHolderBase>? v) in toop) {
68+
int all = types[k];
69+
int bas = all - v.Count;
70+
for (int i = 0; i < v.Count; i++) {
71+
v[i].slot = bas + i;
72+
v[i].knownCount = all;
73+
}
74+
}
75+
}
76+
}
77+
78+
public delegate ref TRet Accessor<T, TRet>(T self) where T : patch_Entity where TRet : class?;
79+
80+
/// <typeparam name="T">Target entity type.</typeparam>
81+
/// <typeparam name="TRet">Attached data type.</typeparam>
82+
public static class DataComponentRegistry<T, TRet> where T : patch_Entity where TRet : class? {
83+
internal class SlotHolder : SlotHolderBase {
84+
internal ref TRet? ReadSlot(T self) {
85+
int slot1 = slot;
86+
if (slot1 < 0) {
87+
Throw();
88+
}
89+
ref object[] slots = ref self.slots;
90+
if (slots is not { }) {
91+
slots = new object[knownCount];
92+
} else if (slots.Length <= slot) {
93+
Array.Resize(ref slots, knownCount);
94+
}
95+
return ref Unsafe.As<object, TRet?>(ref slots[slot1]);
96+
}
97+
98+
internal override Type declaringType => typeof(T);
99+
internal override Type fieldType => typeof(TRet);
100+
}
101+
102+
internal class DebugModeSlotHolder {
103+
internal required int depth;
104+
internal required int slot;
105+
internal required List<DebugModeDataComponentInfo> registered;
106+
internal ref TRet? ReadSlot(T self) {
107+
self.debugSlots ??= new object[getHierarchyDepth(self.GetType())][];
108+
ref object?[]? curDepth = ref self.debugSlots[depth];
109+
110+
if (curDepth is not { }) {
111+
curDepth = new object[registered.Count];
112+
} else if (curDepth.Length <= slot) {
113+
Array.Resize(ref curDepth, registered.Count);
114+
}
115+
// type check is necessary because you can unregister a slot
116+
ref object? got = ref curDepth[slot];
117+
if (got is { } && got is not TRet) {
118+
throw new InvalidCastException();
119+
}
120+
return ref Unsafe.As<object?, TRet?>(ref got);
121+
}
122+
}
123+
internal static Accessor<T, TRet?> RegisterForDebug(DataComponentInfo? info) {
124+
if (!debugInfos.TryGetValue(typeof(T), out List<DebugModeDataComponentInfo>? regList)) {
125+
regList = new();
126+
debugInfos.Add(typeof(T), regList);
127+
}
128+
129+
DebugModeDataComponentInfo debugInfo = new() { info = info, knownUnloaded = -1, unloaded = false };
130+
// TODO: Optimize it to use the linked list which is not implemented yet
131+
if (regList.FindIndex(d => d.unloaded) is { } i and >= 0) {
132+
regList[i] = debugInfo;
133+
} else {
134+
i = regList.Count;
135+
regList.Add(debugInfo);
136+
}
137+
138+
return new DebugModeSlotHolder() { slot = i, registered = regList, depth = getHierarchyDepth(typeof(T)) - 1, }.ReadSlot;
139+
}
140+
141+
internal static Accessor<T, TRet?> RegisterFor(DataComponentInfo? info) {
142+
if (toOptimize is not { } toop) {
143+
throw new InvalidOperationException("Slots have been frozen.");
144+
}
145+
if (!toop.TryGetValue(typeof(T), out List<SlotHolderBase>? holderList)) {
146+
holderList = new();
147+
toop.Add(typeof(T), holderList);
148+
}
149+
if (!infos.TryGetValue(typeof(T), out List<DataComponentInfo?>? regList)) {
150+
regList = new();
151+
infos.Add(typeof(T), regList);
152+
}
153+
var slot = new SlotHolder();
154+
regList.Add(info);
155+
holderList.Add(slot);
156+
return slot.ReadSlot;
157+
}
158+
159+
/// <summary>
160+
/// A performant data holder implementation.
161+
/// Allows you to attach any data to a type of entity.
162+
/// </summary>
163+
/// <remarks>
164+
/// note: in debug mode, register new field and then access it
165+
/// *may* invalidate all existing debug references.
166+
/// this can secretly happens in hook chain.
167+
/// be careful with this one.
168+
/// <br/>
169+
/// nobody except code reloading should be doing this, and it should not happen during Update.
170+
/// </remarks>
171+
/// <param name="info">
172+
/// it's mainly for external tools, and not actually used anywhere.
173+
/// type your modname and comment here.
174+
/// </param>
175+
/// <param name="debug">
176+
/// debug mode will enable type check, dynamic register and unregister.
177+
/// it's not free, so please disable them when publishing.
178+
/// <br/>
179+
/// a good idea is declare your own wrapper method like this:
180+
/// <code>
181+
/// public static Accessor&lt;T, TRet&gt; RegisterFor&lt;T, TRet&gt;(RegistryInfo? info) where T : Entity where TRet : class? {
182+
/// #if DEBUG
183+
/// bool debug = true;
184+
/// #else
185+
/// bool debug = false;
186+
/// #endif
187+
/// DataComponentRegistry.RegisterFor&lt;T, TRet&gt;(info, debug);
188+
/// }
189+
/// </code>
190+
/// </param>
191+
/// <returns>The field accessor. note that your field can be null if it's not initialized.</returns>
192+
public static Accessor<T, TRet?> RegisterFor(DataComponentInfo? info, bool debug) {
193+
if (debug) {
194+
return RegisterForDebug(info);
195+
} else {
196+
return RegisterFor(info);
197+
}
198+
}
199+
200+
internal class GetterSetterWrapper {
201+
internal readonly Accessor<T, TRet?> accessor;
202+
203+
public GetterSetterWrapper(Accessor<T, TRet?> accessor) {
204+
this.accessor = accessor;
205+
}
206+
207+
internal TRet? Getter(T self) => accessor(self);
208+
internal void Setter(T self, TRet? value) => accessor(self) = value;
209+
}
210+
211+
/// <remarks>
212+
/// returns simple getter and setter.
213+
/// </remarks>
214+
/// <inheritdoc cref="RegisterFor(DataComponentInfo?, bool)"/>
215+
public static (Action<T, TRet?> setter, Func<T, TRet?> getter) RegisterForSimple(DataComponentInfo? info, bool debug) {
216+
var reader = new GetterSetterWrapper(RegisterFor(info, debug));
217+
return (reader.Setter, reader.Getter);
218+
}
219+
220+
/// <inheritdoc cref="Unregister(Accessor{T, TRet?})"/>
221+
public static void Unregister(Func<T, TRet?> getter) {
222+
if (getter.Target is not GetterSetterWrapper wrapper) {
223+
throw new ArgumentException("Where did you get this accessor?");
224+
}
225+
Unregister(wrapper.accessor);
226+
}
227+
/// <inheritdoc cref="Unregister(Accessor{T, TRet?})"/>
228+
public static void Unregister(Action<T, TRet?> setter) {
229+
if (setter.Target is not GetterSetterWrapper wrapper) {
230+
throw new ArgumentException("Where did you get this accessor?");
231+
}
232+
Unregister(wrapper.accessor);
233+
}
234+
/// <summary>
235+
/// You can only unload slots which is registered under debug mode.
236+
/// </summary>
237+
/// <remarks>
238+
/// a good idea is declare your own wrapper method like this:
239+
/// <code>
240+
/// public static void Unregister&lt;T, TRet&gt;(Accessor&lt;T, TRet&gt; accessor) where T : Entity where TRet : class? {
241+
/// #if DEBUG
242+
/// DataComponentRegistry&lt;T, TRet&gt;.Unregister(accessor);
243+
/// #endif
244+
/// }
245+
/// </code>
246+
/// </remarks>
247+
public static void Unregister(Accessor<T, TRet?> accessor) {
248+
if (accessor.Target is not DebugModeSlotHolder holder) {
249+
if (accessor.Target is SlotHolderBase) {
250+
throw new ArgumentException("Can't unregister release slot.");
251+
}
252+
throw new ArgumentException("Where did you get this accessor?");
253+
}
254+
Span<DebugModeDataComponentInfo> reg = CollectionsMarshal.AsSpan(holder.registered);
255+
reg[holder.slot].unloaded = true;
256+
// TODO: Update knownUnloaded to make a linked list so it's faster
257+
// if anybody care about the loading performance
258+
}
259+
}
260+
}

Celeste.Mod.mm/Patches/Monocle/Entity.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
using System.ComponentModel;
66

77
namespace Monocle {
8-
class patch_Entity : Entity {
8+
public class patch_Entity : Entity {
9+
internal object[] slots = null;
10+
internal object[][] debugSlots = null;
11+
912
public new Scene Scene {
1013
[MonoModIgnore]
1114
get;

0 commit comments

Comments
 (0)