From 99869181d861369f6843748ca6455e73345904bb Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sat, 7 Jun 2025 18:49:23 -0400 Subject: [PATCH 01/30] kinda working MPB management? --- Source/DynamicProperties/CompiledProps.cs | 81 +++++++++++++ .../MaterialPropertyManager.cs | 65 +++++++++++ .../Patches/NoDuplicateMaterials.cs | 74 ++++++++++++ Source/DynamicProperties/Patches/PartPatch.cs | 46 ++++++++ Source/DynamicProperties/Props.cs | 106 ++++++++++++++++++ Source/Shabby.csproj | 11 ++ 6 files changed, 383 insertions(+) create mode 100644 Source/DynamicProperties/CompiledProps.cs create mode 100644 Source/DynamicProperties/MaterialPropertyManager.cs create mode 100644 Source/DynamicProperties/Patches/NoDuplicateMaterials.cs create mode 100644 Source/DynamicProperties/Patches/PartPatch.cs create mode 100644 Source/DynamicProperties/Props.cs diff --git a/Source/DynamicProperties/CompiledProps.cs b/Source/DynamicProperties/CompiledProps.cs new file mode 100644 index 0000000..84d861e --- /dev/null +++ b/Source/DynamicProperties/CompiledProps.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Shabby; + +#nullable enable + +internal class MpbCacheEntry +{ + internal readonly MaterialPropertyBlock Mpb = new(); + internal readonly Dictionary> ManagedIds = []; +} + +internal class CompiledProps +{ + private static readonly IEqualityComparer> CascadeKeyComparer = + SortedSet.CreateSetComparer(); + + // FIXME: clear old entries... + private static readonly Dictionary, MpbCacheEntry> mpbCache = + new(CascadeKeyComparer); + + internal static void Clear() => mpbCache.Clear(); + + private readonly SortedSet cascade = new(Props.PriorityComparer); + + internal bool Add(Props props) + { + cachedMpb = null; + return cascade.Add(props); + } + + private MaterialPropertyBlock? cachedMpb = null; + + // Should this be a hashset? + private static readonly List _dirtyProps = []; + + internal static void UpdateDirtyProps() + { + foreach (var (cascade, cache) in mpbCache) { + foreach (var props in cascade) { + if (!props.Dirty) continue; + _dirtyProps.Add(props); + foreach (var managedId in cache.ManagedIds[props]) { + props.Write(managedId, cache.Mpb); + } + } + } + + foreach (var props in _dirtyProps) props.Dirty = false; + _dirtyProps.Clear(); + } + + internal MaterialPropertyBlock Get() + { + if (cachedMpb != null) return cachedMpb; + + if (!mpbCache.TryGetValue(cascade, out var cacheEntry)) { + mpbCache[cascade] = cacheEntry = new MpbCacheEntry(); + + Dictionary idManagers = []; + foreach (var props in cascade) { + foreach (var id in props.ManagedIds) { + idManagers[id] = props; + } + } + + foreach (var (id, props) in idManagers) { + if (!cacheEntry.ManagedIds.TryGetValue(props, out var ids)) { + cacheEntry.ManagedIds[props] = ids = []; + } + + ids.Add(id); + props.Write(id, cacheEntry.Mpb); + } + } + + cachedMpb = cacheEntry.Mpb; + return cachedMpb; + } +} diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs new file mode 100644 index 0000000..1d4177f --- /dev/null +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Shabby; + +[KSPScenario( + createOptions: ScenarioCreationOptions.AddToAllGames, + tgtScenes: [GameScenes.LOADING, GameScenes.EDITOR, GameScenes.FLIGHT])] +public sealed class MaterialPropertyManager : ScenarioModule +{ + #region Fields + + public static MaterialPropertyManager Instance { get; private set; } + + private readonly Dictionary compiledProperties = []; + + #endregion + + #region Lifecycle + + public override void OnAwake() + { + name = nameof(MaterialPropertyManager); + Instance = this; + } + + private void LateUpdate() => Refresh(); + + public void OnDestroy() + { + Instance = null; + CompiledProps.Clear(); + } + + #endregion + + public void Set(Renderer renderer, Props props) + { + if (!compiledProperties.TryGetValue(renderer, out var compiledProps)) { + compiledProperties[renderer] = compiledProps = new CompiledProps(); + } + + compiledProps.Add(props); + } + + private static readonly List _deadRenderers = []; + + private void Refresh() + { + CompiledProps.UpdateDirtyProps(); + + foreach (var (renderer, compiledProps) in compiledProperties) { + if (renderer == null) { + _deadRenderers.Add(renderer); + continue; + } + + renderer.SetPropertyBlock(compiledProps.Get()); + } + + foreach (var dead in _deadRenderers) { + compiledProperties.Remove(dead); + } + } +} diff --git a/Source/DynamicProperties/Patches/NoDuplicateMaterials.cs b/Source/DynamicProperties/Patches/NoDuplicateMaterials.cs new file mode 100644 index 0000000..1e0736d --- /dev/null +++ b/Source/DynamicProperties/Patches/NoDuplicateMaterials.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using HarmonyLib; +using Highlighting; +using KSPBuildTools; +using UnityEngine; + +namespace Shabby; + +// [HarmonyPatch(typeof(Renderer))] +// internal static class MaterialAccessWatchdog +// { +// [HarmonyPatch(nameof(Renderer.materials), MethodType.Getter)] +// [HarmonyPostfix] +// internal static void Renderer_materials_get_Postfix() +// { +// var trace = new StackTrace(); +// foreach (var frame in trace.GetFrames()!) { +// var type = frame.GetMethod()?.DeclaringType; +// if (type == typeof(KSP.UI.Screens.EditorPartIcon) || +// type == typeof(PSystemManager) || +// type == typeof(PSystemSetup) || +// type == typeof(Upgradeables.UpgradeableObject) || +// type == typeof(KSP.UI.Screens.Flight.NavBall)) { +// return; +// } +// } +// +// Log.Debug($"Called `Renderer.materials`\n{trace}"); +// } +// } + +[HarmonyPatch] +internal static class NoDuplicateMaterials +{ + private static readonly MethodInfo mInfo_Renderer_material_get = + AccessTools.PropertyGetter(typeof(Renderer), nameof(Renderer.material)); + + private static readonly MethodInfo mInfo_Renderer_materials_get = + AccessTools.PropertyGetter(typeof(Renderer), nameof(Renderer.materials)); + + private static readonly MethodInfo mInfo_Renderer_sharedMaterial_get = + AccessTools.PropertyGetter(typeof(Renderer), nameof(Renderer.sharedMaterial)); + + private static readonly MethodInfo mInfo_Renderer_sharedMaterials_get = + AccessTools.PropertyGetter(typeof(Renderer), nameof(Renderer.sharedMaterials)); + + private static IEnumerable TargetMethods() => [ + AccessTools.Method(typeof(Highlighter), "GrabRenderers"), + AccessTools.Method(typeof(MaterialColorUpdater), "CreateRendererList"), + AccessTools.Method(typeof(ModuleColorChanger), "ProcessMaterialsList"), + AccessTools.Method( + typeof(GameObjectExtension), nameof(GameObjectExtension.SetLayerRecursive), + [typeof(GameObject), typeof(int), typeof(bool), typeof(int)]) + ]; + + [HarmonyTranspiler] + internal static IEnumerable MaterialToSharedMaterialTranspiler( + MethodBase targetMethod, IEnumerable instructions) + { + foreach (var insn in instructions) { + if (insn.Calls(mInfo_Renderer_material_get)) { + insn.operand = mInfo_Renderer_sharedMaterial_get; + Log.Debug("patched `Renderer.material` getter"); + } else if (insn.Calls(mInfo_Renderer_materials_get)) { + insn.operand = mInfo_Renderer_sharedMaterials_get; + Log.Debug("patched `Renderer.materials` getter"); + } + + yield return insn; + } + } +} diff --git a/Source/DynamicProperties/Patches/PartPatch.cs b/Source/DynamicProperties/Patches/PartPatch.cs new file mode 100644 index 0000000..afab48a --- /dev/null +++ b/Source/DynamicProperties/Patches/PartPatch.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using HarmonyLib; + +namespace Shabby; + +[HarmonyPatch(typeof(Part))] +internal static class PartPatch +{ + private static readonly Dictionary highlightProperties = []; + + [HarmonyPostfix] + [HarmonyPatch("Awake")] + private static void Awake_Postfix(Part __instance) + { + highlightProperties[__instance] = new Props(int.MinValue + 1); + } + + [HarmonyPostfix] + [HarmonyPatch("CreateRendererLists")] + private static void CreateRendererLists_Postfix(Part __instance) + { + var props = highlightProperties[__instance]; + props.SetFloat(PropertyIDs._RimFalloff, 2f); + props.SetColor(PropertyIDs._RimColor, Part.defaultHighlightNone); + foreach (var renderer in __instance.HighlightRenderer) { + MaterialPropertyManager.Instance.Set(renderer, props); + } + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(Part.SetOpacity))] + private static bool SetOpacity_Prefix(Part __instance, float opacity) + { + __instance.CreateRendererLists(); + __instance.mpb.SetFloat(PropertyIDs._Opacity, opacity); + highlightProperties[__instance].SetFloat(PropertyIDs._Opacity, opacity); + return false; + } + + [HarmonyPostfix] + [HarmonyPatch("OnDestroy")] + private static void OnDestroy_Postfix(Part __instance) + { + highlightProperties.Remove(__instance); + } +} diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs new file mode 100644 index 0000000..c52349c --- /dev/null +++ b/Source/DynamicProperties/Props.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using KSPBuildTools; +using UnityEngine; + +namespace Shabby; + +internal abstract class Prop; + +internal class Prop(T value) : Prop +{ + internal T Value = value; + + public override string ToString() => Value.ToString(); +} + +public sealed class Props(int priority) +{ + public readonly int Priority = priority; + + private readonly Dictionary _props = []; + + internal bool Dirty = false; + + private static uint _idCounter = 0; + private static uint _nextId() => _idCounter++; + private readonly uint _uniqueId = _nextId(); + + // Note that this is compatible with default object reference equality. + public static readonly Comparer PriorityComparer = Comparer.Create((a, b) => + { + var priorityCmp = a.Priority.CompareTo(b.Priority); + return priorityCmp != 0 ? priorityCmp : a._uniqueId.CompareTo(b._uniqueId); + }); + + internal IEnumerable ManagedIds => _props.Keys; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void _internalSet(int id, T value) + { + Dirty = true; + + MaterialPropertyManager.Instance.LogDebug($"setting {id} to {value}"); + + if (!_props.TryGetValue(id, out var prop)) { + _props[id] = new Prop(value); + return; + } + + if (prop is not Prop propT) { + MaterialPropertyManager.Instance.LogWarning( + $"property {id} has mismatched type; overwriting with {typeof(T).Name}!"); + _props[id] = new Prop(value); + return; + } + + propT.Value = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetColor(int id, Color value) => _internalSet(id, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetFloat(int id, float value) => _internalSet(id, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetInt(int id, int value) => _internalSet(id, value); + + public void SetTexture(int id, Texture value) => _internalSet(id, value); + public void SetVector(int id, Vector4 value) => _internalSet(id, value); + + private bool _internalHas(int id) => _props.TryGetValue(id, out var prop) && prop is Prop; + + public bool HasColor(int id) => _internalHas(id); + public bool HasFloat(int id) => _internalHas(id); + public bool HasInt(int id) => _internalHas(id); + public bool HasTexture(int id) => _internalHas(id); + public bool HasVector(int id) => _internalHas(id); + + internal void Write(int id, MaterialPropertyBlock mpb) + { + if (!_props.TryGetValue(id, out var prop)) { + throw new KeyNotFoundException($"property {id} not found"); + } + + switch (prop) { + case Prop c: mpb.SetColor(id, c.Value); break; + case Prop f: mpb.SetFloat(id, f.Value); break; + case Prop i: mpb.SetInt(id, i.Value); break; + case Prop t: mpb.SetTexture(id, t.Value); break; + case Prop v: mpb.SetVector(id, v.Value); break; + } + } + + public override string ToString() + { + var sb = StringBuilderCache.Acquire(); + sb.AppendFormat("(Priority {0}) {{\n", Priority); + foreach (var (id, prop) in _props) { + sb.AppendFormat("{0} = {1}\n", id, prop); + } + + sb.AppendLine("}"); + return sb.ToStringAndRelease(); + } +} diff --git a/Source/Shabby.csproj b/Source/Shabby.csproj index 6bcfd1d..6f25deb 100644 --- a/Source/Shabby.csproj +++ b/Source/Shabby.csproj @@ -3,6 +3,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -37,6 +41,13 @@ + + + + + + + From ac616a7af895caf9504edf91f455dbc170abc85b Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sat, 7 Jun 2025 21:55:40 -0400 Subject: [PATCH 02/30] better change tracking, and don't mutate the cache key... --- Source/DynamicProperties/CompiledProps.cs | 78 ++++++++++++------- .../MaterialPropertyManager.cs | 6 +- Source/DynamicProperties/Props.cs | 7 +- 3 files changed, 58 insertions(+), 33 deletions(-) diff --git a/Source/DynamicProperties/CompiledProps.cs b/Source/DynamicProperties/CompiledProps.cs index 84d861e..6e5e208 100644 --- a/Source/DynamicProperties/CompiledProps.cs +++ b/Source/DynamicProperties/CompiledProps.cs @@ -9,6 +9,7 @@ internal class MpbCacheEntry { internal readonly MaterialPropertyBlock Mpb = new(); internal readonly Dictionary> ManagedIds = []; + internal bool Changed = true; } internal class CompiledProps @@ -17,65 +18,82 @@ internal class CompiledProps SortedSet.CreateSetComparer(); // FIXME: clear old entries... - private static readonly Dictionary, MpbCacheEntry> mpbCache = + private static readonly Dictionary, MpbCacheEntry> MpbCache = new(CascadeKeyComparer); - internal static void Clear() => mpbCache.Clear(); + internal static void Clear() => MpbCache.Clear(); private readonly SortedSet cascade = new(Props.PriorityComparer); internal bool Add(Props props) { - cachedMpb = null; - return cascade.Add(props); + var added = cascade.Add(props); + if (added) cacheEntry = null; + return added; } - private MaterialPropertyBlock? cachedMpb = null; + private MpbCacheEntry? cacheEntry = null; // Should this be a hashset? - private static readonly List _dirtyProps = []; + private static readonly List _changedProps = []; - internal static void UpdateDirtyProps() + internal static void RefreshChangedProps() { - foreach (var (cascade, cache) in mpbCache) { + foreach (var (cascade, cache) in MpbCache) { + cache.Changed = false; foreach (var props in cascade) { - if (!props.Dirty) continue; - _dirtyProps.Add(props); + if (!props.Changed) continue; + cache.Changed = true; + _changedProps.Add(props); foreach (var managedId in cache.ManagedIds[props]) { props.Write(managedId, cache.Mpb); } } } - foreach (var props in _dirtyProps) props.Dirty = false; - _dirtyProps.Clear(); + foreach (var props in _changedProps) props.Changed = false; + _changedProps.Clear(); } - internal MaterialPropertyBlock Get() + private static MpbCacheEntry BuildCacheEntry(SortedSet cascade) { - if (cachedMpb != null) return cachedMpb; + var clonedCascade = new SortedSet(cascade, Props.PriorityComparer); + var entry = MpbCache[clonedCascade] = new MpbCacheEntry(); - if (!mpbCache.TryGetValue(cascade, out var cacheEntry)) { - mpbCache[cascade] = cacheEntry = new MpbCacheEntry(); + Dictionary idManagers = []; + foreach (var props in cascade) { + foreach (var id in props.ManagedIds) { + idManagers[id] = props; + } + } - Dictionary idManagers = []; - foreach (var props in cascade) { - foreach (var id in props.ManagedIds) { - idManagers[id] = props; - } + foreach (var (id, props) in idManagers) { + if (!entry.ManagedIds.TryGetValue(props, out var ids)) { + entry.ManagedIds[props] = ids = []; } - foreach (var (id, props) in idManagers) { - if (!cacheEntry.ManagedIds.TryGetValue(props, out var ids)) { - cacheEntry.ManagedIds[props] = ids = []; - } + ids.Add(id); + props.Write(id, entry.Mpb); + } - ids.Add(id); - props.Write(id, cacheEntry.Mpb); - } + return entry; + } + + internal bool GetIfChanged(out MaterialPropertyBlock? mpb) + { + if (cacheEntry != null) { + mpb = cacheEntry.Changed ? cacheEntry.Mpb : null; + return cacheEntry.Changed; + } + + if (!MpbCache.TryGetValue(cascade, out cacheEntry)) { + Debug.Log("cache not hit"); + cacheEntry = BuildCacheEntry(cascade); + } else { + Debug.Log("cache hit!"); } - cachedMpb = cacheEntry.Mpb; - return cachedMpb; + mpb = cacheEntry.Mpb; + return true; } } diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 1d4177f..1dc1ccb 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -47,7 +47,7 @@ public void Set(Renderer renderer, Props props) private void Refresh() { - CompiledProps.UpdateDirtyProps(); + CompiledProps.RefreshChangedProps(); foreach (var (renderer, compiledProps) in compiledProperties) { if (renderer == null) { @@ -55,7 +55,9 @@ private void Refresh() continue; } - renderer.SetPropertyBlock(compiledProps.Get()); + if (compiledProps.GetIfChanged(out var mpb)) { + renderer.SetPropertyBlock(mpb); + } } foreach (var dead in _deadRenderers) { diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index c52349c..63cef6d 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -20,7 +20,7 @@ public sealed class Props(int priority) private readonly Dictionary _props = []; - internal bool Dirty = false; + internal bool Changed = false; private static uint _idCounter = 0; private static uint _nextId() => _idCounter++; @@ -44,6 +44,7 @@ private void _internalSet(int id, T value) if (!_props.TryGetValue(id, out var prop)) { _props[id] = new Prop(value); + Changed = true; return; } @@ -51,10 +52,14 @@ private void _internalSet(int id, T value) MaterialPropertyManager.Instance.LogWarning( $"property {id} has mismatched type; overwriting with {typeof(T).Name}!"); _props[id] = new Prop(value); + Changed = true; return; } + if (EqualityComparer.Default.Equals(value, propT.Value)) return; + propT.Value = value; + Changed = true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 06c5e21edbb5dbfdc0c6b4934e79c4e74336a73f Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sat, 7 Jun 2025 22:20:38 -0400 Subject: [PATCH 03/30] a best-effort attempt at recovering property names for diagnostics --- Source/DynamicProperties/Props.cs | 56 +++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 63cef6d..9d11b42 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -1,10 +1,53 @@ using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using KSPBuildTools; using UnityEngine; namespace Shabby; +internal static class PropIdToName +{ + private static readonly string[] CommonProperties = [ + "TransparentFX", + "_BumpMap", + "_Color", + "_EmissiveColor", + "_MainTex", + "_MaxX", + "_MaxY", + "_MinX", + "_MinY", + "_Multiplier", + "_Opacity", + "_RimColor", + "_RimFalloff", + "_TC1Color", + "_TC1MetalBlend", + "_TC1Metalness", + "_TC1SmoothBlend", + "_TC1Smoothness", + "_TC2Color", + "_TC2MetalBlend", + "_TC2Metalness", + "_TC2SmoothBlend", + "_TC2Smoothness", + "_TemperatureColor", + "_Tex", + "_Tint", + "_TintColor", + "_subdiv", + "localMatrix", + "upMatrix" + ]; + + private static readonly Dictionary IdToName = + CommonProperties.ToDictionary(Shader.PropertyToID, name => name); + + internal static string Get(int id) => + IdToName.TryGetValue(id, out var name) ? name : $"<{id}>"; +} + internal abstract class Prop; internal class Prop(T value) : Prop @@ -38,10 +81,6 @@ public sealed class Props(int priority) [MethodImpl(MethodImplOptions.AggressiveInlining)] private void _internalSet(int id, T value) { - Dirty = true; - - MaterialPropertyManager.Instance.LogDebug($"setting {id} to {value}"); - if (!_props.TryGetValue(id, out var prop)) { _props[id] = new Prop(value); Changed = true; @@ -50,7 +89,7 @@ private void _internalSet(int id, T value) if (prop is not Prop propT) { MaterialPropertyManager.Instance.LogWarning( - $"property {id} has mismatched type; overwriting with {typeof(T).Name}!"); + $"property {PropIdToName.Get(id)} has mismatched type; overwriting with {typeof(T).Name}!"); _props[id] = new Prop(value); Changed = true; return; @@ -85,9 +124,12 @@ private void _internalSet(int id, T value) internal void Write(int id, MaterialPropertyBlock mpb) { if (!_props.TryGetValue(id, out var prop)) { - throw new KeyNotFoundException($"property {id} not found"); + throw new KeyNotFoundException($"property {PropIdToName.Get(id)} not found"); } + MaterialPropertyManager.Instance.LogDebug( + $"writing property {PropIdToName.Get(id)} = {prop}"); + switch (prop) { case Prop c: mpb.SetColor(id, c.Value); break; case Prop f: mpb.SetFloat(id, f.Value); break; @@ -102,7 +144,7 @@ public override string ToString() var sb = StringBuilderCache.Acquire(); sb.AppendFormat("(Priority {0}) {{\n", Priority); foreach (var (id, prop) in _props) { - sb.AppendFormat("{0} = {1}\n", id, prop); + sb.AppendFormat("{0} = {1}\n", PropIdToName.Get(id), prop); } sb.AppendLine("}"); From bbd62ec850a03091b581c660265360faa4f99eda Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sat, 7 Jun 2025 23:17:14 -0400 Subject: [PATCH 04/30] just use dynamic dispatch --- Source/DynamicProperties/Props.cs | 76 +++++++++++++++++++------------ 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 9d11b42..78827a2 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -48,15 +49,42 @@ internal static string Get(int id) => IdToName.TryGetValue(id, out var name) ? name : $"<{id}>"; } -internal abstract class Prop; +internal abstract class Prop +{ + internal abstract void Write(int id, MaterialPropertyBlock mpb); +} -internal class Prop(T value) : Prop +internal abstract class Prop(T value) : Prop { internal T Value = value; - public override string ToString() => Value.ToString(); } +internal class PropColor(Color value) : Prop(value) +{ + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetColor(id, Value); +} + +internal class PropFloat(float value) : Prop(value) +{ + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetFloat(id, Value); +} + +internal class PropInt(int value) : Prop(value) +{ + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetInt(id, Value); +} + +internal class PropTexture(Texture value) : Prop(value) +{ + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetTexture(id, Value); +} + +internal class PropVector(Vector4 value) : Prop(value) +{ + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetVector(id, Value); +} + public sealed class Props(int priority) { public readonly int Priority = priority; @@ -79,39 +107,36 @@ public sealed class Props(int priority) internal IEnumerable ManagedIds => _props.Keys; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void _internalSet(int id, T value) + private void _internalSet(int id, T value) where TProp : Prop { - if (!_props.TryGetValue(id, out var prop)) { - _props[id] = new Prop(value); - Changed = true; - return; - } + if (_props.TryGetValue(id, out var prop)) { + if (prop is TProp typedProp) { + if (EqualityComparer.Default.Equals(value, typedProp.Value)) return; + + typedProp.Value = value; + Changed = true; + return; + } - if (prop is not Prop propT) { MaterialPropertyManager.Instance.LogWarning( $"property {PropIdToName.Get(id)} has mismatched type; overwriting with {typeof(T).Name}!"); - _props[id] = new Prop(value); - Changed = true; - return; } - if (EqualityComparer.Default.Equals(value, propT.Value)) return; - - propT.Value = value; + _props[id] = (TProp)Activator.CreateInstance(typeof(TProp), value); Changed = true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetColor(int id, Color value) => _internalSet(id, value); + public void SetColor(int id, Color value) => _internalSet(id, value); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetFloat(int id, float value) => _internalSet(id, value); + public void SetFloat(int id, float value) => _internalSet(id, value); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetInt(int id, int value) => _internalSet(id, value); + public void SetInt(int id, int value) => _internalSet(id, value); - public void SetTexture(int id, Texture value) => _internalSet(id, value); - public void SetVector(int id, Vector4 value) => _internalSet(id, value); + public void SetTexture(int id, Texture value) => _internalSet(id, value); + public void SetVector(int id, Vector4 value) => _internalSet(id, value); private bool _internalHas(int id) => _props.TryGetValue(id, out var prop) && prop is Prop; @@ -121,6 +146,7 @@ private void _internalSet(int id, T value) public bool HasTexture(int id) => _internalHas(id); public bool HasVector(int id) => _internalHas(id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Write(int id, MaterialPropertyBlock mpb) { if (!_props.TryGetValue(id, out var prop)) { @@ -130,13 +156,7 @@ internal void Write(int id, MaterialPropertyBlock mpb) MaterialPropertyManager.Instance.LogDebug( $"writing property {PropIdToName.Get(id)} = {prop}"); - switch (prop) { - case Prop c: mpb.SetColor(id, c.Value); break; - case Prop f: mpb.SetFloat(id, f.Value); break; - case Prop i: mpb.SetInt(id, i.Value); break; - case Prop t: mpb.SetTexture(id, t.Value); break; - case Prop v: mpb.SetVector(id, v.Value); break; - } + prop.Write(id, mpb); } public override string ToString() From 4c86094b1016ae6407be3cda00c7662344985948 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 8 Jun 2025 15:07:59 -0400 Subject: [PATCH 05/30] patch Part::Highlight --- Source/DynamicProperties/Patches/PartPatch.cs | 81 +++++++++++++++++++ Source/DynamicProperties/Props.cs | 18 ++--- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/Source/DynamicProperties/Patches/PartPatch.cs b/Source/DynamicProperties/Patches/PartPatch.cs index afab48a..8e9cda1 100644 --- a/Source/DynamicProperties/Patches/PartPatch.cs +++ b/Source/DynamicProperties/Patches/PartPatch.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Reflection.Emit; using HarmonyLib; +using UnityEngine; namespace Shabby; @@ -37,6 +39,85 @@ private static bool SetOpacity_Prefix(Part __instance, float opacity) return false; } + private static void Highlight_SetRimColor(Part part, Color color) + { + highlightProperties[part].SetColor(PropertyIDs._RimColor, color); + } + + [HarmonyTranspiler] + [HarmonyPatch(nameof(Part.Highlight), typeof(Color))] + private static IEnumerable Highlight_Transpiler( + IEnumerable insns) + { + var MPB_SetColor = AccessTools.Method( + typeof(MaterialPropertyBlock), + nameof(MaterialPropertyBlock.SetColor), + [typeof(int)]); + var Part_get_mpb = AccessTools.PropertyGetter(typeof(Part), nameof(Part.mpb)); + var Part_highlightRenderer = + AccessTools.Field(typeof(Part), nameof(Part.highlightRenderer)); + var PropertyIDs__RimColor = + AccessTools.Field(typeof(PropertyIDs), nameof(PropertyIDs._RimColor)); + var Renderer_SetPropertyBlock = AccessTools.Method( + typeof(Renderer), + nameof(Renderer.SetPropertyBlock), + [typeof(MaterialPropertyBlock)]); + + CodeMatch[] matchDupPop = [new(OpCodes.Dup), new(OpCodes.Pop)]; + + // mpb.SetColor(PropertyIDs._RimColor, value); + // IL_0049: ldarg.0 // this + // IL_004a: call instance class UnityEngine.MaterialPropertyBlock Part::get_mpb() + // IL_004f: ldsfld int32 PropertyIDs::_RimColor + // IL_0054: ldloc.0 // color + // IL_0055: callvirt instance void UnityEngine.MaterialPropertyBlock::SetColor(int32, valuetype UnityEngine.Color) + CodeMatch[] matchSetRimColor = [ + new(OpCodes.Ldarg_0), + new(OpCodes.Call, Part_get_mpb), + new(OpCodes.Ldsfld, PropertyIDs__RimColor), + new(OpCodes.Ldloc_0), + new(OpCodes.Callvirt, MPB_SetColor) + ]; + + // highlightRenderer[count].SetPropertyBlock(mpb); + // IL_008a: ldarg.0 // this; jump target + // IL_008b: ldfld class System.Collections.Generic.List`1 Part::highlightRenderer + // IL_0090: ldloc.1 // count + // IL_0091: callvirt instance !0/*class UnityEngine.Renderer*/ class System.Collections.Generic.List`1::get_Item(int32) + // IL_0096: ldarg.0 // this + // IL_0097: call instance class UnityEngine.MaterialPropertyBlock Part::get_mpb() + // IL_009c: callvirt instance void UnityEngine.Renderer::SetPropertyBlock(class UnityEngine.MaterialPropertyBlock) + CodeMatch[] matchSetMpb = [ + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Part_highlightRenderer), + new(OpCodes.Ldloc_1), + new(OpCodes.Callvirt), // can't easily specify indexer... + new(OpCodes.Ldarg_0), + new(OpCodes.Call, Part_get_mpb), + new(OpCodes.Callvirt, Renderer_SetPropertyBlock) + ]; + + var matcher = new CodeMatcher(insns); + matcher + .MatchStartForward(matchDupPop) + .Repeat(cm => cm.RemoveInstructions(matchDupPop.Length)) + .Start() + .MatchStartForward(matchSetRimColor) + .ThrowIfNotMatch("failed to find MPB set _RimColor call") + .RemoveInstructions(matchSetRimColor.Length) + .InsertAndAdvance( + // PartPatch.Highlight_SetRimColor(this, value); + new CodeInstruction(OpCodes.Ldarg_0), // `this` + new CodeInstruction(OpCodes.Ldloc_0), // `value` + CodeInstruction.Call(() => Highlight_SetRimColor(default, default))) + .MatchStartForward(matchSetMpb) + .ThrowIfNotMatch("failed to find Renderer.SetMPB call") + // No need to replace application, since that is automatic. + .SetAndAdvance(OpCodes.Nop, null) // preserve label + .RemoveInstructions(matchSetMpb.Length - 1); + return matcher.InstructionEnumeration(); + } + [HarmonyPostfix] [HarmonyPatch("OnDestroy")] private static void OnDestroy_Postfix(Part __instance) diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 78827a2..2a711fe 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -89,27 +89,27 @@ public sealed class Props(int priority) { public readonly int Priority = priority; - private readonly Dictionary _props = []; + private readonly Dictionary props = []; internal bool Changed = false; private static uint _idCounter = 0; private static uint _nextId() => _idCounter++; - private readonly uint _uniqueId = _nextId(); + private readonly uint uniqueId = _nextId(); // Note that this is compatible with default object reference equality. public static readonly Comparer PriorityComparer = Comparer.Create((a, b) => { var priorityCmp = a.Priority.CompareTo(b.Priority); - return priorityCmp != 0 ? priorityCmp : a._uniqueId.CompareTo(b._uniqueId); + return priorityCmp != 0 ? priorityCmp : a.uniqueId.CompareTo(b.uniqueId); }); - internal IEnumerable ManagedIds => _props.Keys; + internal IEnumerable ManagedIds => props.Keys; [MethodImpl(MethodImplOptions.AggressiveInlining)] private void _internalSet(int id, T value) where TProp : Prop { - if (_props.TryGetValue(id, out var prop)) { + if (props.TryGetValue(id, out var prop)) { if (prop is TProp typedProp) { if (EqualityComparer.Default.Equals(value, typedProp.Value)) return; @@ -122,7 +122,7 @@ private void _internalSet(int id, T value) where TProp : Prop $"property {PropIdToName.Get(id)} has mismatched type; overwriting with {typeof(T).Name}!"); } - _props[id] = (TProp)Activator.CreateInstance(typeof(TProp), value); + props[id] = (TProp)Activator.CreateInstance(typeof(TProp), value); Changed = true; } @@ -138,7 +138,7 @@ private void _internalSet(int id, T value) where TProp : Prop public void SetTexture(int id, Texture value) => _internalSet(id, value); public void SetVector(int id, Vector4 value) => _internalSet(id, value); - private bool _internalHas(int id) => _props.TryGetValue(id, out var prop) && prop is Prop; + private bool _internalHas(int id) => props.TryGetValue(id, out var prop) && prop is Prop; public bool HasColor(int id) => _internalHas(id); public bool HasFloat(int id) => _internalHas(id); @@ -149,7 +149,7 @@ private void _internalSet(int id, T value) where TProp : Prop [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Write(int id, MaterialPropertyBlock mpb) { - if (!_props.TryGetValue(id, out var prop)) { + if (!props.TryGetValue(id, out var prop)) { throw new KeyNotFoundException($"property {PropIdToName.Get(id)} not found"); } @@ -163,7 +163,7 @@ public override string ToString() { var sb = StringBuilderCache.Acquire(); sb.AppendFormat("(Priority {0}) {{\n", Priority); - foreach (var (id, prop) in _props) { + foreach (var (id, prop) in props) { sb.AppendFormat("{0} = {1}\n", PropIdToName.Get(id), prop); } From 4374883ec5c9290ddb6cf3b86fa896dfd0326ca8 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 8 Jun 2025 16:05:54 -0400 Subject: [PATCH 06/30] put the property manager in a namespace --- Source/DynamicProperties/CompiledProps.cs | 2 +- Source/DynamicProperties/MaterialPropertyManager.cs | 2 +- Source/DynamicProperties/Patches/PartPatch.cs | 2 +- Source/DynamicProperties/Props.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/DynamicProperties/CompiledProps.cs b/Source/DynamicProperties/CompiledProps.cs index 6e5e208..b087e63 100644 --- a/Source/DynamicProperties/CompiledProps.cs +++ b/Source/DynamicProperties/CompiledProps.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using UnityEngine; -namespace Shabby; +namespace Shabby.DynamicProperties; #nullable enable diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 1dc1ccb..f1c322f 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using UnityEngine; -namespace Shabby; +namespace Shabby.DynamicProperties; [KSPScenario( createOptions: ScenarioCreationOptions.AddToAllGames, diff --git a/Source/DynamicProperties/Patches/PartPatch.cs b/Source/DynamicProperties/Patches/PartPatch.cs index 8e9cda1..f7f59f3 100644 --- a/Source/DynamicProperties/Patches/PartPatch.cs +++ b/Source/DynamicProperties/Patches/PartPatch.cs @@ -3,7 +3,7 @@ using HarmonyLib; using UnityEngine; -namespace Shabby; +namespace Shabby.DynamicProperties; [HarmonyPatch(typeof(Part))] internal static class PartPatch diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 2a711fe..86ba26c 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -5,7 +5,7 @@ using KSPBuildTools; using UnityEngine; -namespace Shabby; +namespace Shabby.DynamicProperties; internal static class PropIdToName { From 31275971e47c96e6675bad853379195234829290 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 8 Jun 2025 16:06:23 -0400 Subject: [PATCH 07/30] add a remove method to the manager --- Source/DynamicProperties/CompiledProps.cs | 7 +++++++ .../DynamicProperties/MaterialPropertyManager.cs | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Source/DynamicProperties/CompiledProps.cs b/Source/DynamicProperties/CompiledProps.cs index b087e63..ae75dd5 100644 --- a/Source/DynamicProperties/CompiledProps.cs +++ b/Source/DynamicProperties/CompiledProps.cs @@ -32,6 +32,13 @@ internal bool Add(Props props) return added; } + internal bool Remove(Props props) + { + var removed = cascade.Remove(props); + if (removed) cacheEntry = null; + return removed; + } + private MpbCacheEntry? cacheEntry = null; // Should this be a hashset? diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index f1c322f..290983f 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -18,6 +18,10 @@ public sealed class MaterialPropertyManager : ScenarioModule #region Lifecycle + private MaterialPropertyManager() + { + } + public override void OnAwake() { name = nameof(MaterialPropertyManager); @@ -34,13 +38,19 @@ public void OnDestroy() #endregion - public void Set(Renderer renderer, Props props) + public bool Set(Renderer renderer, Props props) { if (!compiledProperties.TryGetValue(renderer, out var compiledProps)) { compiledProperties[renderer] = compiledProps = new CompiledProps(); } - compiledProps.Add(props); + return compiledProps.Add(props); + } + + public bool Remove(Renderer renderer, Props props) + { + if (!compiledProperties.TryGetValue(renderer, out var compiledProps)) return false; + return compiledProps.Remove(props); } private static readonly List _deadRenderers = []; From c46d585ed5ad960e92eea435a4335d6db4ea9ae3 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 8 Jun 2025 20:44:17 -0400 Subject: [PATCH 08/30] switch to KSPAddon --- Source/DynamicProperties/CompiledProps.cs | 28 ++++++++++------- .../MaterialPropertyManager.cs | 30 ++++++++++++------- Source/DynamicProperties/Patches/PartPatch.cs | 14 ++++----- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/Source/DynamicProperties/CompiledProps.cs b/Source/DynamicProperties/CompiledProps.cs index ae75dd5..f7f0955 100644 --- a/Source/DynamicProperties/CompiledProps.cs +++ b/Source/DynamicProperties/CompiledProps.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using KSPBuildTools; using UnityEngine; namespace Shabby.DynamicProperties; @@ -21,9 +22,10 @@ internal class CompiledProps private static readonly Dictionary, MpbCacheEntry> MpbCache = new(CascadeKeyComparer); - internal static void Clear() => MpbCache.Clear(); + internal static void ClearCache() => MpbCache.Clear(); private readonly SortedSet cascade = new(Props.PriorityComparer); + private MpbCacheEntry? cacheEntry = null; internal bool Add(Props props) { @@ -39,21 +41,19 @@ internal bool Remove(Props props) return removed; } - private MpbCacheEntry? cacheEntry = null; - // Should this be a hashset? private static readonly List _changedProps = []; internal static void RefreshChangedProps() { - foreach (var (cascade, cache) in MpbCache) { - cache.Changed = false; + foreach (var (cascade, cacheEntry) in MpbCache) { + cacheEntry.Changed = false; foreach (var props in cascade) { if (!props.Changed) continue; - cache.Changed = true; + cacheEntry.Changed = true; _changedProps.Add(props); - foreach (var managedId in cache.ManagedIds[props]) { - props.Write(managedId, cache.Mpb); + foreach (var managedId in cacheEntry.ManagedIds[props]) { + props.Write(managedId, cacheEntry.Mpb); } } } @@ -90,16 +90,24 @@ internal bool GetIfChanged(out MaterialPropertyBlock? mpb) { if (cacheEntry != null) { mpb = cacheEntry.Changed ? cacheEntry.Mpb : null; + // if (cacheEntry.Changed && HighLogic.LoadedSceneIsEditor) { + // Debug.Log(cascade.Aggregate("props:\n", (current, props) => current + props)); + // } + return cacheEntry.Changed; } if (!MpbCache.TryGetValue(cascade, out cacheEntry)) { - Debug.Log("cache not hit"); + MaterialPropertyManager.Instance.LogDebug("building new MPB"); cacheEntry = BuildCacheEntry(cascade); } else { - Debug.Log("cache hit!"); + MaterialPropertyManager.Instance.LogDebug("MPB cache hit"); } + // if (HighLogic.LoadedSceneIsEditor) { + // Debug.Log(cascade.Aggregate("props:\n", (current, props) => current + props)); + // } + mpb = cacheEntry.Mpb; return true; } diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 290983f..7227cd8 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; +using KSPBuildTools; using UnityEngine; namespace Shabby.DynamicProperties; -[KSPScenario( - createOptions: ScenarioCreationOptions.AddToAllGames, - tgtScenes: [GameScenes.LOADING, GameScenes.EDITOR, GameScenes.FLIGHT])] -public sealed class MaterialPropertyManager : ScenarioModule +[KSPAddon(KSPAddon.Startup.EveryScene, false)] +public sealed class MaterialPropertyManager : MonoBehaviour { #region Fields @@ -22,18 +21,26 @@ private MaterialPropertyManager() { } - public override void OnAwake() + private void Awake() { + if (Instance != null) { + DestroyImmediate(this); + return; + } + name = nameof(MaterialPropertyManager); Instance = this; } private void LateUpdate() => Refresh(); - public void OnDestroy() + private void OnDestroy() { + if (Instance != this) return; + Instance = null; - CompiledProps.Clear(); + CompiledProps.ClearCache(); + this.LogDebug("destroyed"); } #endregion @@ -61,17 +68,20 @@ private void Refresh() foreach (var (renderer, compiledProps) in compiledProperties) { if (renderer == null) { + this.LogDebug($"dead renderer {renderer.GetHashCode()}"); _deadRenderers.Add(renderer); continue; } + if (!renderer.gameObject.activeInHierarchy) continue; + if (compiledProps.GetIfChanged(out var mpb)) { + this.LogDebug($"set mpb on renderer {renderer.name} {renderer.GetHashCode()}\n"); renderer.SetPropertyBlock(mpb); } } - foreach (var dead in _deadRenderers) { - compiledProperties.Remove(dead); - } + foreach (var dead in _deadRenderers) compiledProperties.Remove(dead); + _deadRenderers.Clear(); } } diff --git a/Source/DynamicProperties/Patches/PartPatch.cs b/Source/DynamicProperties/Patches/PartPatch.cs index f7f59f3..43a2dd5 100644 --- a/Source/DynamicProperties/Patches/PartPatch.cs +++ b/Source/DynamicProperties/Patches/PartPatch.cs @@ -8,20 +8,20 @@ namespace Shabby.DynamicProperties; [HarmonyPatch(typeof(Part))] internal static class PartPatch { - private static readonly Dictionary highlightProperties = []; + private static readonly Dictionary rimHighlightProps = []; [HarmonyPostfix] [HarmonyPatch("Awake")] private static void Awake_Postfix(Part __instance) { - highlightProperties[__instance] = new Props(int.MinValue + 1); + rimHighlightProps[__instance] = new Props(int.MinValue + 1); } [HarmonyPostfix] [HarmonyPatch("CreateRendererLists")] private static void CreateRendererLists_Postfix(Part __instance) { - var props = highlightProperties[__instance]; + var props = rimHighlightProps[__instance]; props.SetFloat(PropertyIDs._RimFalloff, 2f); props.SetColor(PropertyIDs._RimColor, Part.defaultHighlightNone); foreach (var renderer in __instance.HighlightRenderer) { @@ -35,13 +35,13 @@ private static bool SetOpacity_Prefix(Part __instance, float opacity) { __instance.CreateRendererLists(); __instance.mpb.SetFloat(PropertyIDs._Opacity, opacity); - highlightProperties[__instance].SetFloat(PropertyIDs._Opacity, opacity); + rimHighlightProps[__instance].SetFloat(PropertyIDs._Opacity, opacity); return false; } private static void Highlight_SetRimColor(Part part, Color color) { - highlightProperties[part].SetColor(PropertyIDs._RimColor, color); + rimHighlightProps[part].SetColor(PropertyIDs._RimColor, color); } [HarmonyTranspiler] @@ -52,7 +52,7 @@ private static IEnumerable Highlight_Transpiler( var MPB_SetColor = AccessTools.Method( typeof(MaterialPropertyBlock), nameof(MaterialPropertyBlock.SetColor), - [typeof(int)]); + [typeof(int), typeof(Color)]); var Part_get_mpb = AccessTools.PropertyGetter(typeof(Part), nameof(Part.mpb)); var Part_highlightRenderer = AccessTools.Field(typeof(Part), nameof(Part.highlightRenderer)); @@ -122,6 +122,6 @@ private static IEnumerable Highlight_Transpiler( [HarmonyPatch("OnDestroy")] private static void OnDestroy_Postfix(Part __instance) { - highlightProperties.Remove(__instance); + rimHighlightProps.Remove(__instance); } } From b05c785219bc92b68ab96eaa32914aa0909b5988 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 12:26:57 -0400 Subject: [PATCH 09/30] rewrite it again ...to a "push" architecture --- Source/DynamicProperties/CompiledProps.cs | 114 -------------- .../MaterialPropertyManager.cs | 53 ++++--- Source/DynamicProperties/MpbCompiler.cs | 143 ++++++++++++++++++ Source/DynamicProperties/Patches/PartPatch.cs | 4 +- Source/DynamicProperties/Props.cs | 56 +++++-- Source/DynamicProperties/PropsCascade.cs | 82 ++++++++++ 6 files changed, 295 insertions(+), 157 deletions(-) delete mode 100644 Source/DynamicProperties/CompiledProps.cs create mode 100644 Source/DynamicProperties/MpbCompiler.cs create mode 100644 Source/DynamicProperties/PropsCascade.cs diff --git a/Source/DynamicProperties/CompiledProps.cs b/Source/DynamicProperties/CompiledProps.cs deleted file mode 100644 index f7f0955..0000000 --- a/Source/DynamicProperties/CompiledProps.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections.Generic; -using KSPBuildTools; -using UnityEngine; - -namespace Shabby.DynamicProperties; - -#nullable enable - -internal class MpbCacheEntry -{ - internal readonly MaterialPropertyBlock Mpb = new(); - internal readonly Dictionary> ManagedIds = []; - internal bool Changed = true; -} - -internal class CompiledProps -{ - private static readonly IEqualityComparer> CascadeKeyComparer = - SortedSet.CreateSetComparer(); - - // FIXME: clear old entries... - private static readonly Dictionary, MpbCacheEntry> MpbCache = - new(CascadeKeyComparer); - - internal static void ClearCache() => MpbCache.Clear(); - - private readonly SortedSet cascade = new(Props.PriorityComparer); - private MpbCacheEntry? cacheEntry = null; - - internal bool Add(Props props) - { - var added = cascade.Add(props); - if (added) cacheEntry = null; - return added; - } - - internal bool Remove(Props props) - { - var removed = cascade.Remove(props); - if (removed) cacheEntry = null; - return removed; - } - - // Should this be a hashset? - private static readonly List _changedProps = []; - - internal static void RefreshChangedProps() - { - foreach (var (cascade, cacheEntry) in MpbCache) { - cacheEntry.Changed = false; - foreach (var props in cascade) { - if (!props.Changed) continue; - cacheEntry.Changed = true; - _changedProps.Add(props); - foreach (var managedId in cacheEntry.ManagedIds[props]) { - props.Write(managedId, cacheEntry.Mpb); - } - } - } - - foreach (var props in _changedProps) props.Changed = false; - _changedProps.Clear(); - } - - private static MpbCacheEntry BuildCacheEntry(SortedSet cascade) - { - var clonedCascade = new SortedSet(cascade, Props.PriorityComparer); - var entry = MpbCache[clonedCascade] = new MpbCacheEntry(); - - Dictionary idManagers = []; - foreach (var props in cascade) { - foreach (var id in props.ManagedIds) { - idManagers[id] = props; - } - } - - foreach (var (id, props) in idManagers) { - if (!entry.ManagedIds.TryGetValue(props, out var ids)) { - entry.ManagedIds[props] = ids = []; - } - - ids.Add(id); - props.Write(id, entry.Mpb); - } - - return entry; - } - - internal bool GetIfChanged(out MaterialPropertyBlock? mpb) - { - if (cacheEntry != null) { - mpb = cacheEntry.Changed ? cacheEntry.Mpb : null; - // if (cacheEntry.Changed && HighLogic.LoadedSceneIsEditor) { - // Debug.Log(cascade.Aggregate("props:\n", (current, props) => current + props)); - // } - - return cacheEntry.Changed; - } - - if (!MpbCache.TryGetValue(cascade, out cacheEntry)) { - MaterialPropertyManager.Instance.LogDebug("building new MPB"); - cacheEntry = BuildCacheEntry(cascade); - } else { - MaterialPropertyManager.Instance.LogDebug("MPB cache hit"); - } - - // if (HighLogic.LoadedSceneIsEditor) { - // Debug.Log(cascade.Aggregate("props:\n", (current, props) => current + props)); - // } - - mpb = cacheEntry.Mpb; - return true; - } -} diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 7227cd8..167ac3d 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -11,7 +11,7 @@ public sealed class MaterialPropertyManager : MonoBehaviour public static MaterialPropertyManager Instance { get; private set; } - private readonly Dictionary compiledProperties = []; + private readonly Dictionary rendererCascades = []; #endregion @@ -32,14 +32,17 @@ private void Awake() Instance = this; } - private void LateUpdate() => Refresh(); - private void OnDestroy() { if (Instance != this) return; Instance = null; - CompiledProps.ClearCache(); + PropsCascade.ClearCache(); + + // Poor man's GC :'( + MaterialColorUpdaterPatch.temperatureColorProps.Clear(); + ModuleColorChangerPatch.mccProps.Clear(); + this.LogDebug("destroyed"); } @@ -47,41 +50,37 @@ private void OnDestroy() public bool Set(Renderer renderer, Props props) { - if (!compiledProperties.TryGetValue(renderer, out var compiledProps)) { - compiledProperties[renderer] = compiledProps = new CompiledProps(); + if (renderer == null) { + Log.LogError(this, $"cannot set property on null renderer {renderer.GetHashCode()}"); + return false; + } + + if (!rendererCascades.TryGetValue(renderer, out var cascade)) { + rendererCascades[renderer] = cascade = new PropsCascade(renderer); } - return compiledProps.Add(props); + return cascade.Add(props); } public bool Remove(Renderer renderer, Props props) { - if (!compiledProperties.TryGetValue(renderer, out var compiledProps)) return false; - return compiledProps.Remove(props); + if (!rendererCascades.TryGetValue(renderer, out var cascade)) return false; + return cascade.Remove(props); } - private static readonly List _deadRenderers = []; - - private void Refresh() + public bool Remove(Renderer renderer) { - CompiledProps.RefreshChangedProps(); + return rendererCascades.Remove(renderer); + } - foreach (var (renderer, compiledProps) in compiledProperties) { - if (renderer == null) { - this.LogDebug($"dead renderer {renderer.GetHashCode()}"); - _deadRenderers.Add(renderer); - continue; - } + public bool Remove(Props props) + { + var removed = false; - if (!renderer.gameObject.activeInHierarchy) continue; + foreach (var cascade in rendererCascades.Values) removed |= cascade.Remove(props); - if (compiledProps.GetIfChanged(out var mpb)) { - this.LogDebug($"set mpb on renderer {renderer.name} {renderer.GetHashCode()}\n"); - renderer.SetPropertyBlock(mpb); - } - } + PropsCascade.RemoveCacheEntriesWith(props); - foreach (var dead in _deadRenderers) compiledProperties.Remove(dead); - _deadRenderers.Clear(); + return removed; } } diff --git a/Source/DynamicProperties/MpbCompiler.cs b/Source/DynamicProperties/MpbCompiler.cs new file mode 100644 index 0000000..848077d --- /dev/null +++ b/Source/DynamicProperties/MpbCompiler.cs @@ -0,0 +1,143 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using KSPBuildTools; +using UnityEngine; + +namespace Shabby.DynamicProperties; + +internal class MpbCompiler : IDisposable +{ + /// Immutable. + internal readonly SortedSet Cascade; + + private readonly HashSet linkedRenderers = []; + private readonly MaterialPropertyBlock mpb = new(); + private readonly Dictionary> idManagerMap = []; + + private static readonly MaterialPropertyBlock EmptyMpb = new(); + + internal MpbCompiler(SortedSet cascades) + { + Cascade = cascades; + RebuildManagerMap(); + RewriteMpb(); + foreach (var props in Cascade) { + props.OnValueChanged += OnPropsValueChanged; + props.OnEntriesChanged += OnPropsEntriesChanged; + } + } + + internal void Register(Renderer renderer) + { + linkedRenderers.Add(renderer); + Apply(renderer); + } + + internal void Unregister(Renderer renderer) + { + linkedRenderers.Remove(renderer); + renderer.SetPropertyBlock(EmptyMpb); + } + + private void RebuildManagerMap() + { + idManagerMap.Clear(); + + Dictionary idManagers = []; + foreach (var props in Cascade) { + foreach (var id in props.ManagedIds) { + idManagers[id] = props; + } + } + + foreach (var (id, props) in idManagers) { + if (!idManagerMap.TryGetValue(props, out var ids)) { + idManagerMap[props] = ids = []; + } + + ids.Add(id); + } + } + + private void OnPropsValueChanged(Props props) + { + WriteMpb(props); + Apply(); + } + + private void OnPropsEntriesChanged(Props props) + { + RebuildManagerMap(); + RewriteMpb(); + Apply(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteMpb(Props props) + { + foreach (var id in idManagerMap[props]) props.Write(id, mpb); + } + + private void RewriteMpb() + { + mpb.Clear(); + foreach (var props in idManagerMap.Keys) WriteMpb(props); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Apply(Renderer renderer) => renderer.SetPropertyBlock(mpb); + + private readonly List _deadRenderers = []; + + internal void Apply() + { + foreach (var renderer in linkedRenderers) { + if (renderer == null) { + _deadRenderers.Add(renderer!); + continue; + } + + Apply(renderer); + } + + foreach (var dead in _deadRenderers) { + MaterialPropertyManager.Instance.LogDebug($"dead renderer {dead.GetHashCode()}"); + MaterialPropertyManager.Instance.Remove(dead); + } + + if (linkedRenderers.Count == 0) { + MaterialPropertyManager.Instance.LogDebug("dead cache entry"); + PropsCascade.RemoveCacheEntry(this); + } + } + + private bool _disposed = false; + + private void UnlinkProps() + { + if (_disposed) return; + + Debug.Log("disposing MPB cache entry"); + + foreach (var props in Cascade) { + props.OnValueChanged -= OnPropsValueChanged; + props.OnEntriesChanged -= OnPropsEntriesChanged; + } + + _disposed = true; + } + + public void Dispose() + { + UnlinkProps(); + GC.SuppressFinalize(this); + } + + ~MpbCompiler() + { + UnlinkProps(); + } +} diff --git a/Source/DynamicProperties/Patches/PartPatch.cs b/Source/DynamicProperties/Patches/PartPatch.cs index 43a2dd5..3def1e7 100644 --- a/Source/DynamicProperties/Patches/PartPatch.cs +++ b/Source/DynamicProperties/Patches/PartPatch.cs @@ -122,6 +122,8 @@ private static IEnumerable Highlight_Transpiler( [HarmonyPatch("OnDestroy")] private static void OnDestroy_Postfix(Part __instance) { - rimHighlightProps.Remove(__instance); + if (rimHighlightProps.Remove(__instance, out var props)) { + props.Dispose(); + } } } diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 86ba26c..c1cafbc 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -85,27 +85,32 @@ internal class PropVector(Vector4 value) : Prop(value) internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetVector(id, Value); } -public sealed class Props(int priority) +public sealed class Props(int priority) : IDisposable { - public readonly int Priority = priority; - - private readonly Dictionary props = []; - - internal bool Changed = false; - - private static uint _idCounter = 0; - private static uint _nextId() => _idCounter++; - private readonly uint uniqueId = _nextId(); - - // Note that this is compatible with default object reference equality. + /// Ordered by lowest to highest priority. Equal priority is disambiguated by unique IDs. + /// Note that this is compatible with default object reference equality. public static readonly Comparer PriorityComparer = Comparer.Create((a, b) => { var priorityCmp = a.Priority.CompareTo(b.Priority); - return priorityCmp != 0 ? priorityCmp : a.uniqueId.CompareTo(b.uniqueId); + return priorityCmp != 0 ? priorityCmp : a.UniqueId.CompareTo(b.UniqueId); }); + private static uint _idCounter = 0; + private static uint _nextId() => _idCounter++; + + public readonly uint UniqueId = _nextId(); + + public readonly int Priority = priority; + + private readonly Dictionary props = []; + internal IEnumerable ManagedIds => props.Keys; + internal delegate void PropsUpdateHandler(Props props); + + internal PropsUpdateHandler OnValueChanged = delegate { }; + internal PropsUpdateHandler OnEntriesChanged = delegate { }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void _internalSet(int id, T value) where TProp : Prop { @@ -114,7 +119,7 @@ private void _internalSet(int id, T value) where TProp : Prop if (EqualityComparer.Default.Equals(value, typedProp.Value)) return; typedProp.Value = value; - Changed = true; + OnValueChanged(this); return; } @@ -123,7 +128,7 @@ private void _internalSet(int id, T value) where TProp : Prop } props[id] = (TProp)Activator.CreateInstance(typeof(TProp), value); - Changed = true; + OnEntriesChanged(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -170,4 +175,25 @@ public override string ToString() sb.AppendLine("}"); return sb.ToStringAndRelease(); } + + private bool _disposed = false; + + private void UnregisterSelf(bool disposing) + { + if (_disposed) return; + Debug.Log($"disposing Props instance {UniqueId}"); + if (disposing) MaterialPropertyManager.Instance?.Remove(this); + _disposed = true; + } + + public void Dispose() + { + UnregisterSelf(true); + GC.SuppressFinalize(this); + } + + ~Props() + { + UnregisterSelf(false); + } } diff --git a/Source/DynamicProperties/PropsCascade.cs b/Source/DynamicProperties/PropsCascade.cs new file mode 100644 index 0000000..637a4ae --- /dev/null +++ b/Source/DynamicProperties/PropsCascade.cs @@ -0,0 +1,82 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using KSPBuildTools; +using UnityEngine; + +namespace Shabby.DynamicProperties; + +internal class PropsCascade(Renderer renderer) +{ + internal readonly Renderer Renderer = renderer; + + private static readonly IEqualityComparer> CacheKeyComparer = + SortedSet.CreateSetComparer(); // Object equality is fine. + + private static readonly Dictionary, MpbCompiler> MpbCache = + new(CacheKeyComparer); + + internal static void ClearCache() => MpbCache.Clear(); + + private readonly SortedSet cascade = new(Props.PriorityComparer); + private MpbCompiler? mpbCompiler = null; + + internal static void RemoveCacheEntry(MpbCompiler entry) + { + MpbCache.Remove(entry.Cascade); + entry.Dispose(); + } + + private static readonly List _entriesToRemove = []; + + internal static void RemoveCacheEntriesWith(Props props) + { + foreach (var entry in MpbCache) { + if (entry.Key.Contains(props)) _entriesToRemove.Add(entry.Value); + } + + foreach (var entry in _entriesToRemove) RemoveCacheEntry(entry); + _entriesToRemove.Clear(); + } + + private void ReacquireCompiler() + { + mpbCompiler?.Unregister(Renderer); + + if (!MpbCache.TryGetValue(cascade, out mpbCompiler)) { + MaterialPropertyManager.Instance.LogDebug("building new cache entry"); + + // Don't accidentally mutate the cache key... + var clonedCascade = new SortedSet(cascade, Props.PriorityComparer); + mpbCompiler = new MpbCompiler(clonedCascade); +#if DEBUG + if (!(!ReferenceEquals(cascade, mpbCompiler.Cascade) && + CacheKeyComparer.Equals(cascade, mpbCompiler.Cascade))) { + throw new InvalidOperationException("cache key equality check failed"); + } +#endif + MpbCache[mpbCompiler.Cascade] = mpbCompiler; + } else { + MaterialPropertyManager.Instance.LogDebug("cache hit"); + } + + mpbCompiler.Register(Renderer); + } + + internal bool Add(Props props) + { + if (!cascade.Add(props)) return false; + + ReacquireCompiler(); + return true; + } + + internal bool Remove(Props props) + { + if (!cascade.Remove(props)) return false; + + ReacquireCompiler(); + return true; + } +} From 4a4a335a32c738b3b097371e3bfef472ccbbbf45 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 12:27:35 -0400 Subject: [PATCH 10/30] add a mechanism to suppress eager updates --- .../MaterialPropertyManager.cs | 32 +++++++++++++++++++ Source/DynamicProperties/Props.cs | 25 +++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 167ac3d..16006bc 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -13,6 +13,8 @@ public sealed class MaterialPropertyManager : MonoBehaviour private readonly Dictionary rendererCascades = []; + private readonly List propsLateUpdateQueue = []; + #endregion #region Lifecycle @@ -83,4 +85,34 @@ public bool Remove(Props props) return removed; } + + private bool _propRefreshScheduled = false; + private static readonly WaitForEndOfFrame WfEoF = new(); + + private IEnumerator Co_propsLateUpdate() + { + yield return WfEoF; + + foreach (var props in propsLateUpdateQueue) { + if (props.NeedsEntriesUpdate) { + props.OnEntriesChanged(props); + } else if (props.NeedsValueUpdate) { + props.OnValueChanged(props); + } + + props.SuppressEagerUpdate = + props.NeedsEntriesUpdate = props.NeedsValueUpdate = false; + } + + propsLateUpdateQueue.Clear(); + _propRefreshScheduled = false; + } + + internal void ScheduleLateUpdate(Props props) + { + propsLateUpdateQueue.Add(props); + if (_propRefreshScheduled) return; + StartCoroutine(Co_propsLateUpdate()); + _propRefreshScheduled = true; + } } diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index c1cafbc..3c3fe1e 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -111,6 +111,16 @@ public sealed class Props(int priority) : IDisposable internal PropsUpdateHandler OnValueChanged = delegate { }; internal PropsUpdateHandler OnEntriesChanged = delegate { }; + internal bool SuppressEagerUpdate = false; + internal bool NeedsValueUpdate = false; + internal bool NeedsEntriesUpdate = false; + + public void SuppressEagerUpdatesThisFrame() + { + SuppressEagerUpdate = true; + MaterialPropertyManager.Instance.ScheduleLateUpdate(this); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void _internalSet(int id, T value) where TProp : Prop { @@ -119,7 +129,13 @@ private void _internalSet(int id, T value) where TProp : Prop if (EqualityComparer.Default.Equals(value, typedProp.Value)) return; typedProp.Value = value; - OnValueChanged(this); + + if (!SuppressEagerUpdate) { + OnValueChanged(this); + } else { + NeedsValueUpdate = true; + } + return; } @@ -128,7 +144,12 @@ private void _internalSet(int id, T value) where TProp : Prop } props[id] = (TProp)Activator.CreateInstance(typeof(TProp), value); - OnEntriesChanged(this); + + if (!SuppressEagerUpdate) { + OnEntriesChanged(this); + } else { + NeedsEntriesUpdate = true; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] From d82b20538bbea9d357d79c85bf5c19d952fa61a2 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 14:12:15 -0400 Subject: [PATCH 11/30] use approx equality for prop value updates --- Source/DynamicProperties/Props.cs | 45 +++++++++++++++++++++++++++---- Source/DynamicProperties/Utils.cs | 41 ++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 Source/DynamicProperties/Utils.cs diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 3c3fe1e..59b3dba 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -57,31 +57,68 @@ internal abstract class Prop internal abstract class Prop(T value) : Prop { internal T Value = value; + + internal abstract bool UpdateIfChanged(T value); public override string ToString() => Value.ToString(); } internal class PropColor(Color value) : Prop(value) { + internal override bool UpdateIfChanged(Color value) + { + if (Utils.ApproxEquals(value, Value)) return false; + Value = value; + return true; + } + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetColor(id, Value); } internal class PropFloat(float value) : Prop(value) { + internal override bool UpdateIfChanged(float value) + { + if (Utils.ApproxEqualsRel(value, Value)) return false; + Value = value; + return true; + } + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetFloat(id, Value); } internal class PropInt(int value) : Prop(value) { + internal override bool UpdateIfChanged(int value) + { + if (value == Value) return false; + Value = value; + return true; + } + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetInt(id, Value); } internal class PropTexture(Texture value) : Prop(value) { + internal override bool UpdateIfChanged(Texture value) + { + if (ReferenceEquals(value, Value)) return false; + Value = value; + return true; + } + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetTexture(id, Value); } internal class PropVector(Vector4 value) : Prop(value) { + internal override bool UpdateIfChanged(Vector4 value) + { + if (Utils.ApproxEqualsRel(value, Value)) return false; + Value = value; + return true; + } + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetVector(id, Value); } @@ -118,7 +155,7 @@ public sealed class Props(int priority) : IDisposable public void SuppressEagerUpdatesThisFrame() { SuppressEagerUpdate = true; - MaterialPropertyManager.Instance.ScheduleLateUpdate(this); + MaterialPropertyManager.Instance?.ScheduleLateUpdate(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -126,9 +163,7 @@ private void _internalSet(int id, T value) where TProp : Prop { if (props.TryGetValue(id, out var prop)) { if (prop is TProp typedProp) { - if (EqualityComparer.Default.Equals(value, typedProp.Value)) return; - - typedProp.Value = value; + if (!typedProp.UpdateIfChanged(value)) return; if (!SuppressEagerUpdate) { OnValueChanged(this); @@ -179,7 +214,7 @@ internal void Write(int id, MaterialPropertyBlock mpb) throw new KeyNotFoundException($"property {PropIdToName.Get(id)} not found"); } - MaterialPropertyManager.Instance.LogDebug( + MaterialPropertyManager.Instance?.LogDebug( $"writing property {PropIdToName.Get(id)} = {prop}"); prop.Write(id, mpb); diff --git a/Source/DynamicProperties/Utils.cs b/Source/DynamicProperties/Utils.cs new file mode 100644 index 0000000..204ee69 --- /dev/null +++ b/Source/DynamicProperties/Utils.cs @@ -0,0 +1,41 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Shabby.DynamicProperties; + +public static class Utils +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ApproxEqualsAbs(float a, float b, float eps) => + Math.Abs(b - a) <= eps; + + /// https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ApproxEqualsRel(float a, float b, + float absDiff = 1e-4f, float relDiff = float.Epsilon) + { + if (a == b) return true; + + var diff = Math.Abs(a - b); + if (diff < absDiff) return true; + + a = Math.Abs(a); + b = Math.Abs(b); + var largest = b > a ? b : a; + return diff <= largest * relDiff; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ApproxEqualsRel(Vector4 a, Vector4 b, + float absDiff = 1e-4f, float relDiff = float.Epsilon) => + ApproxEqualsRel(a.x, b.x, absDiff, relDiff) && + ApproxEqualsRel(a.y, b.y, absDiff, relDiff) && + ApproxEqualsRel(a.z, b.z, absDiff, relDiff) && + ApproxEqualsRel(a.w, b.w, absDiff, relDiff); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ApproxEquals(Color a, Color b, float eps = 1e-2f) => + ApproxEqualsAbs(a.r, b.r, eps) && ApproxEqualsAbs(a.g, b.g, eps) && + ApproxEqualsAbs(a.b, b.b, eps) && ApproxEqualsAbs(a.a, b.a, eps); +} From ce983c41130586a57a5573b1ef9e70c5157bb5ae Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 14:35:53 -0400 Subject: [PATCH 12/30] check dead cache entry more eagerly --- Source/DynamicProperties/MpbCompiler.cs | 43 ++++++++++++++++++++---- Source/DynamicProperties/PropsCascade.cs | 2 -- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Source/DynamicProperties/MpbCompiler.cs b/Source/DynamicProperties/MpbCompiler.cs index 848077d..36633bf 100644 --- a/Source/DynamicProperties/MpbCompiler.cs +++ b/Source/DynamicProperties/MpbCompiler.cs @@ -10,6 +10,8 @@ namespace Shabby.DynamicProperties; internal class MpbCompiler : IDisposable { + #region Fields + /// Immutable. internal readonly SortedSet Cascade; @@ -19,8 +21,13 @@ internal class MpbCompiler : IDisposable private static readonly MaterialPropertyBlock EmptyMpb = new(); + #endregion + internal MpbCompiler(SortedSet cascades) { + MaterialPropertyManager.Instance?.LogDebug( + $"new cache entry {RuntimeHelpers.GetHashCode(this)}"); + Cascade = cascades; RebuildManagerMap(); RewriteMpb(); @@ -30,6 +37,8 @@ internal MpbCompiler(SortedSet cascades) } } + #region Renderer registration + internal void Register(Renderer renderer) { linkedRenderers.Add(renderer); @@ -40,8 +49,21 @@ internal void Unregister(Renderer renderer) { linkedRenderers.Remove(renderer); renderer.SetPropertyBlock(EmptyMpb); + CheckLiveness(); } + private void CheckLiveness() + { + if (linkedRenderers.Count > 0) return; + MaterialPropertyManager.Instance.LogDebug( + $"dead cache entry {RuntimeHelpers.GetHashCode(this)}"); + PropsCascade.RemoveCacheEntry(this); + } + + #endregion + + #region Props updates + private void RebuildManagerMap() { idManagerMap.Clear(); @@ -65,16 +87,20 @@ private void RebuildManagerMap() private void OnPropsValueChanged(Props props) { WriteMpb(props); - Apply(); + ApplyAll(); } private void OnPropsEntriesChanged(Props props) { RebuildManagerMap(); RewriteMpb(); - Apply(); + ApplyAll(); } + #endregion + + #region Apply + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void WriteMpb(Props props) { @@ -92,7 +118,7 @@ private void RewriteMpb() private readonly List _deadRenderers = []; - internal void Apply() + private void ApplyAll() { foreach (var renderer in linkedRenderers) { if (renderer == null) { @@ -108,12 +134,13 @@ internal void Apply() MaterialPropertyManager.Instance.Remove(dead); } - if (linkedRenderers.Count == 0) { - MaterialPropertyManager.Instance.LogDebug("dead cache entry"); - PropsCascade.RemoveCacheEntry(this); - } + CheckLiveness(); } + #endregion + + #region dtor + private bool _disposed = false; private void UnlinkProps() @@ -140,4 +167,6 @@ public void Dispose() { UnlinkProps(); } + + #endregion } diff --git a/Source/DynamicProperties/PropsCascade.cs b/Source/DynamicProperties/PropsCascade.cs index 637a4ae..bb22d40 100644 --- a/Source/DynamicProperties/PropsCascade.cs +++ b/Source/DynamicProperties/PropsCascade.cs @@ -45,8 +45,6 @@ private void ReacquireCompiler() mpbCompiler?.Unregister(Renderer); if (!MpbCache.TryGetValue(cascade, out mpbCompiler)) { - MaterialPropertyManager.Instance.LogDebug("building new cache entry"); - // Don't accidentally mutate the cache key... var clonedCascade = new SortedSet(cascade, Props.PriorityComparer); mpbCompiler = new MpbCompiler(clonedCascade); From 25f189c0b59ed8b1aa10394e6783b8abed18dc60 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 14:36:08 -0400 Subject: [PATCH 13/30] fix NRE --- Source/DynamicProperties/Patches/PartPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/DynamicProperties/Patches/PartPatch.cs b/Source/DynamicProperties/Patches/PartPatch.cs index 3def1e7..b538d4c 100644 --- a/Source/DynamicProperties/Patches/PartPatch.cs +++ b/Source/DynamicProperties/Patches/PartPatch.cs @@ -25,7 +25,7 @@ private static void CreateRendererLists_Postfix(Part __instance) props.SetFloat(PropertyIDs._RimFalloff, 2f); props.SetColor(PropertyIDs._RimColor, Part.defaultHighlightNone); foreach (var renderer in __instance.HighlightRenderer) { - MaterialPropertyManager.Instance.Set(renderer, props); + MaterialPropertyManager.Instance?.Set(renderer, props); } } From c23ba5111728bf0069c45798169f98dbd03c3a3e Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 14:36:23 -0400 Subject: [PATCH 14/30] begin patching MCC and MCU --- .../Patches/MaterialColorUpdaterPatch.cs | 95 +++++++++++++++++++ .../Patches/ModuleColorChangerPatch.cs | 46 +++++++++ Source/Shabby.csproj | 9 +- 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs create mode 100644 Source/DynamicProperties/Patches/ModuleColorChangerPatch.cs diff --git a/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs b/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs new file mode 100644 index 0000000..f0f8d82 --- /dev/null +++ b/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using HarmonyLib; +using UnityEngine; + +namespace Shabby.DynamicProperties; + +[HarmonyPatch(typeof(MaterialColorUpdater))] +public class MaterialColorUpdaterPatch +{ + internal static readonly Dictionary temperatureColorProps = []; + + [HarmonyPostfix] + [HarmonyPatch(MethodType.Constructor, typeof(Transform), typeof(int), typeof(Part))] + private static void MaterialColorUpdater_Ctor_Postfix(MaterialColorUpdater __instance) + { + temperatureColorProps[__instance] = new Props(int.MinValue + 1); + } + + [HarmonyPostfix] + [HarmonyPatch("CreateRendererList")] + private static void MaterialColorUpdater_CreateRendererList_Postfix( + MaterialColorUpdater __instance) + { + var props = temperatureColorProps[__instance]; + foreach (var renderer in __instance.renderers) { + MaterialPropertyManager.Instance?.Set(renderer, props); + } + } + + private static void Update_SetProperty(MaterialColorUpdater mcu) + { + temperatureColorProps[mcu].SetColor(mcu.propertyID, mcu.setColor); + } + + [HarmonyTranspiler] + [HarmonyPatch(nameof(MaterialColorUpdater.Update))] + private static IEnumerable Update_Transpiler( + IEnumerable insns) + { + var MPB_SetColor = AccessTools.Method( + typeof(MaterialPropertyBlock), + nameof(MaterialPropertyBlock.SetColor), + [typeof(int), typeof(Color)]); + + foreach (var insn in insns) { + yield return insn; + + // IL_0022: ldarg.0 // this + // IL_0023: ldfld class UnityEngine.MaterialPropertyBlock MaterialColorUpdater::mpb + // IL_0028: ldarg.0 // this + // IL_0029: ldfld int32 MaterialColorUpdater::propertyID + // IL_002e: ldarg.0 // this + // IL_002f: ldfld valuetype UnityEngine.Color MaterialColorUpdater::setColor + // IL_0034: callvirt instance void UnityEngine.MaterialPropertyBlock::SetColor(int32, valuetype UnityEngine.Color) + if (insn.Calls(MPB_SetColor)) break; + } + + CodeInstruction[] replace = [ + new(OpCodes.Ldarg_0), // this + CodeInstruction.Call(() => Update_SetProperty(default)), + new(OpCodes.Ret) + ]; + foreach (var insn in replace) yield return insn; + } + + private static void DisposeIfExists(MaterialColorUpdater mcu) + { + if (mcu == null) return; + if (temperatureColorProps.TryGetValue(mcu, out var props)) props.Dispose(); + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(Part), nameof(Part.ResetMPB))] + private static void Part_ResetMPB_Prefix(Part __instance) + { + DisposeIfExists(__instance.temperatureRenderer); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(Part), "OnDestroy")] + private static void Part_OnDestroy_Postfix(Part __instance) + { + DisposeIfExists(__instance.temperatureRenderer); + } + + // FIXME: write a transpiler for ModuleJettison.Jettison. + + [HarmonyPostfix] + [HarmonyPatch(typeof(ModuleJettison), "OnDestroy")] + private static void ModuleJettison_OnDestroy_Postfix(ModuleJettison __instance) + { + DisposeIfExists(__instance.jettisonTemperatureRenderer); + } +} diff --git a/Source/DynamicProperties/Patches/ModuleColorChangerPatch.cs b/Source/DynamicProperties/Patches/ModuleColorChangerPatch.cs new file mode 100644 index 0000000..75ae723 --- /dev/null +++ b/Source/DynamicProperties/Patches/ModuleColorChangerPatch.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using HarmonyLib; + +namespace Shabby.DynamicProperties; + +[HarmonyPatch(typeof(ModuleColorChanger))] +internal class ModuleColorChangerPatch +{ + internal static readonly Dictionary mccProps = []; + + [HarmonyPostfix] + [HarmonyPatch(nameof(ModuleColorChanger.OnStart))] + private static void OnStart_Postfix(ModuleColorChanger __instance) + { + mccProps[__instance] = new Props(0); + } + + [HarmonyPostfix] + [HarmonyPatch("EditRenderers")] + private static void EditRenderers_Postfix(ModuleColorChanger __instance) + { + var props = mccProps[__instance]; + foreach (var renderer in __instance.renderers) { + MaterialPropertyManager.Instance?.Set(renderer, props); + } + } + + [HarmonyPrefix] + [HarmonyPatch("UpdateColor")] + public static bool UpdateColor_Prefix(ModuleColorChanger __instance) + { + mccProps[__instance].SetColor(__instance.shaderPropertyInt, __instance.color); + return false; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(Part), "OnDestroy")] + private static void Part_OnDestroy_Postfix(Part __instance) + { + foreach (var mcc in __instance.FindModulesImplementing()) { + if (mccProps.Remove(mcc, out var props)) props.Dispose(); + } + } + + // FIXME: are part modules destroyed in other places? Icon renderers? Drag cube renderers? +} diff --git a/Source/Shabby.csproj b/Source/Shabby.csproj index 6f25deb..a584f93 100644 --- a/Source/Shabby.csproj +++ b/Source/Shabby.csproj @@ -43,9 +43,14 @@ - - + + + + + + + From 11fdd9721d09d255d3110722aab86a3c582b97fa Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 14:50:36 -0400 Subject: [PATCH 15/30] move Prop to a separate file --- Source/DynamicProperties/Prop.cs | 120 ++++++++++++++++++++++++++++++ Source/DynamicProperties/Props.cs | 116 ----------------------------- 2 files changed, 120 insertions(+), 116 deletions(-) create mode 100644 Source/DynamicProperties/Prop.cs diff --git a/Source/DynamicProperties/Prop.cs b/Source/DynamicProperties/Prop.cs new file mode 100644 index 0000000..0d3e36a --- /dev/null +++ b/Source/DynamicProperties/Prop.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Shabby.DynamicProperties; + +internal static class PropIdToName +{ + private static readonly string[] CommonProperties = [ + "TransparentFX", + "_BumpMap", + "_Color", + "_EmissiveColor", + "_MainTex", + "_MaxX", + "_MaxY", + "_MinX", + "_MinY", + "_Multiplier", + "_Opacity", + "_RimColor", + "_RimFalloff", + "_TC1Color", + "_TC1MetalBlend", + "_TC1Metalness", + "_TC1SmoothBlend", + "_TC1Smoothness", + "_TC2Color", + "_TC2MetalBlend", + "_TC2Metalness", + "_TC2SmoothBlend", + "_TC2Smoothness", + "_TemperatureColor", + "_Tex", + "_Tint", + "_TintColor", + "_subdiv", + "localMatrix", + "upMatrix" + ]; + + private static readonly Dictionary IdToName = + CommonProperties.ToDictionary(Shader.PropertyToID, name => name); + + internal static string Get(int id) => + IdToName.TryGetValue(id, out var name) ? name : $"<{id}>"; +} + +internal abstract class Prop +{ + internal abstract void Write(int id, MaterialPropertyBlock mpb); +} + +internal abstract class Prop(T value) : Prop +{ + internal T Value = value; + + internal abstract bool UpdateIfChanged(T value); + public override string ToString() => Value.ToString(); +} + +internal class PropColor(Color value) : Prop(value) +{ + internal override bool UpdateIfChanged(Color value) + { + if (Utils.ApproxEquals(value, Value)) return false; + Value = value; + return true; + } + + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetColor(id, Value); +} + +internal class PropFloat(float value) : Prop(value) +{ + internal override bool UpdateIfChanged(float value) + { + if (Utils.ApproxEqualsRel(value, Value)) return false; + Value = value; + return true; + } + + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetFloat(id, Value); +} + +internal class PropInt(int value) : Prop(value) +{ + internal override bool UpdateIfChanged(int value) + { + if (value == Value) return false; + Value = value; + return true; + } + + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetInt(id, Value); +} + +internal class PropTexture(Texture value) : Prop(value) +{ + internal override bool UpdateIfChanged(Texture value) + { + if (ReferenceEquals(value, Value)) return false; + Value = value; + return true; + } + + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetTexture(id, Value); +} + +internal class PropVector(Vector4 value) : Prop(value) +{ + internal override bool UpdateIfChanged(Vector4 value) + { + if (Utils.ApproxEqualsRel(value, Value)) return false; + Value = value; + return true; + } + + internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetVector(id, Value); +} diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 59b3dba..5d9e358 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -1,127 +1,11 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using KSPBuildTools; using UnityEngine; namespace Shabby.DynamicProperties; -internal static class PropIdToName -{ - private static readonly string[] CommonProperties = [ - "TransparentFX", - "_BumpMap", - "_Color", - "_EmissiveColor", - "_MainTex", - "_MaxX", - "_MaxY", - "_MinX", - "_MinY", - "_Multiplier", - "_Opacity", - "_RimColor", - "_RimFalloff", - "_TC1Color", - "_TC1MetalBlend", - "_TC1Metalness", - "_TC1SmoothBlend", - "_TC1Smoothness", - "_TC2Color", - "_TC2MetalBlend", - "_TC2Metalness", - "_TC2SmoothBlend", - "_TC2Smoothness", - "_TemperatureColor", - "_Tex", - "_Tint", - "_TintColor", - "_subdiv", - "localMatrix", - "upMatrix" - ]; - - private static readonly Dictionary IdToName = - CommonProperties.ToDictionary(Shader.PropertyToID, name => name); - - internal static string Get(int id) => - IdToName.TryGetValue(id, out var name) ? name : $"<{id}>"; -} - -internal abstract class Prop -{ - internal abstract void Write(int id, MaterialPropertyBlock mpb); -} - -internal abstract class Prop(T value) : Prop -{ - internal T Value = value; - - internal abstract bool UpdateIfChanged(T value); - public override string ToString() => Value.ToString(); -} - -internal class PropColor(Color value) : Prop(value) -{ - internal override bool UpdateIfChanged(Color value) - { - if (Utils.ApproxEquals(value, Value)) return false; - Value = value; - return true; - } - - internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetColor(id, Value); -} - -internal class PropFloat(float value) : Prop(value) -{ - internal override bool UpdateIfChanged(float value) - { - if (Utils.ApproxEqualsRel(value, Value)) return false; - Value = value; - return true; - } - - internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetFloat(id, Value); -} - -internal class PropInt(int value) : Prop(value) -{ - internal override bool UpdateIfChanged(int value) - { - if (value == Value) return false; - Value = value; - return true; - } - - internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetInt(id, Value); -} - -internal class PropTexture(Texture value) : Prop(value) -{ - internal override bool UpdateIfChanged(Texture value) - { - if (ReferenceEquals(value, Value)) return false; - Value = value; - return true; - } - - internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetTexture(id, Value); -} - -internal class PropVector(Vector4 value) : Prop(value) -{ - internal override bool UpdateIfChanged(Vector4 value) - { - if (Utils.ApproxEqualsRel(value, Value)) return false; - Value = value; - return true; - } - - internal override void Write(int id, MaterialPropertyBlock mpb) => mpb.SetVector(id, Value); -} - public sealed class Props(int priority) : IDisposable { /// Ordered by lowest to highest priority. Equal priority is disambiguated by unique IDs. From 0ad8a66cd45395dc7d719688a25513b8def7410f Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 15:00:31 -0400 Subject: [PATCH 16/30] add a public API for registering debug id => name entries --- .../MaterialPropertyManager.cs | 12 ++++++++++- Source/DynamicProperties/Prop.cs | 20 +++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 16006bc..42e4fc4 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -50,6 +50,8 @@ private void OnDestroy() #endregion + #region Public API + public bool Set(Renderer renderer, Props props) { if (renderer == null) { @@ -75,7 +77,15 @@ public bool Remove(Renderer renderer) return rendererCascades.Remove(renderer); } - public bool Remove(Props props) + public static void RegisterPropertyNamesForDebugLogging(params string[] properties) + { + foreach (var property in properties) PropIdToName.Register(property); + } + + #endregion + + /// Public API equivalent is calling `Props.Dispose`. + internal bool Remove(Props props) { var removed = false; diff --git a/Source/DynamicProperties/Prop.cs b/Source/DynamicProperties/Prop.cs index 0d3e36a..becddac 100644 --- a/Source/DynamicProperties/Prop.cs +++ b/Source/DynamicProperties/Prop.cs @@ -6,8 +6,7 @@ namespace Shabby.DynamicProperties; internal static class PropIdToName { - private static readonly string[] CommonProperties = [ - "TransparentFX", + private static readonly string[] StockProperties = [ "_BumpMap", "_Color", "_EmissiveColor", @@ -20,19 +19,7 @@ internal static class PropIdToName "_Opacity", "_RimColor", "_RimFalloff", - "_TC1Color", - "_TC1MetalBlend", - "_TC1Metalness", - "_TC1SmoothBlend", - "_TC1Smoothness", - "_TC2Color", - "_TC2MetalBlend", - "_TC2Metalness", - "_TC2SmoothBlend", - "_TC2Smoothness", "_TemperatureColor", - "_Tex", - "_Tint", "_TintColor", "_subdiv", "localMatrix", @@ -40,7 +27,10 @@ internal static class PropIdToName ]; private static readonly Dictionary IdToName = - CommonProperties.ToDictionary(Shader.PropertyToID, name => name); + StockProperties.ToDictionary(Shader.PropertyToID, name => name); + + internal static void Register(string property) => + IdToName[Shader.PropertyToID(property)] = property; internal static string Get(int id) => IdToName.TryGetValue(id, out var name) ? name : $"<{id}>"; From bcf5c77f10e7ad2931a0d2888929e2f439a0af0f Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 15:17:23 -0400 Subject: [PATCH 17/30] per-id differencing --- .../MaterialPropertyManager.cs | 2 +- Source/DynamicProperties/MpbCompiler.cs | 25 +++++++++++++------ Source/DynamicProperties/Props.cs | 10 +++++--- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 42e4fc4..0564c7e 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -107,7 +107,7 @@ private IEnumerator Co_propsLateUpdate() if (props.NeedsEntriesUpdate) { props.OnEntriesChanged(props); } else if (props.NeedsValueUpdate) { - props.OnValueChanged(props); + props.OnValueChanged(props, null); } props.SuppressEagerUpdate = diff --git a/Source/DynamicProperties/MpbCompiler.cs b/Source/DynamicProperties/MpbCompiler.cs index 36633bf..bf42538 100644 --- a/Source/DynamicProperties/MpbCompiler.cs +++ b/Source/DynamicProperties/MpbCompiler.cs @@ -84,16 +84,16 @@ private void RebuildManagerMap() } } - private void OnPropsValueChanged(Props props) + private void OnPropsEntriesChanged(Props props) { - WriteMpb(props); + RebuildManagerMap(); + RewriteMpb(); ApplyAll(); } - private void OnPropsEntriesChanged(Props props) + private void OnPropsValueChanged(Props props, int? id) { - RebuildManagerMap(); - RewriteMpb(); + WriteMpb(props, id); ApplyAll(); } @@ -102,15 +102,24 @@ private void OnPropsEntriesChanged(Props props) #region Apply [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteMpb(Props props) + private void WriteMpb(Props props, int? id) { - foreach (var id in idManagerMap[props]) props.Write(id, mpb); + if (id.HasValue) { + props.Write(id.GetValueOrDefault(), mpb); + } else { + foreach (var managedId in idManagerMap[props]) props.Write(managedId, mpb); + } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RewriteMpb() { mpb.Clear(); - foreach (var props in idManagerMap.Keys) WriteMpb(props); + foreach (var (props, managedIds) in idManagerMap) { + foreach (var managedId in managedIds) { + props.Write(managedId, mpb); + } + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 5d9e358..856621d 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -27,10 +27,12 @@ public sealed class Props(int priority) : IDisposable internal IEnumerable ManagedIds => props.Keys; - internal delegate void PropsUpdateHandler(Props props); + internal delegate void ValueChangedHandler(Props props, int? id); - internal PropsUpdateHandler OnValueChanged = delegate { }; - internal PropsUpdateHandler OnEntriesChanged = delegate { }; + internal delegate void EntriesChangedHandler(Props props); + + internal ValueChangedHandler OnValueChanged = delegate { }; + internal EntriesChangedHandler OnEntriesChanged = delegate { }; internal bool SuppressEagerUpdate = false; internal bool NeedsValueUpdate = false; @@ -50,7 +52,7 @@ private void _internalSet(int id, T value) where TProp : Prop if (!typedProp.UpdateIfChanged(value)) return; if (!SuppressEagerUpdate) { - OnValueChanged(this); + OnValueChanged(this, id); } else { NeedsValueUpdate = true; } From bcd6b0ec31d0b721872ec7a10e364b8cc1ce5750 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 15:20:27 -0400 Subject: [PATCH 18/30] suppress eager updates on Props creation presumably, multiple properties will be set --- Source/DynamicProperties/Props.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 856621d..7c08658 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -6,7 +6,7 @@ namespace Shabby.DynamicProperties; -public sealed class Props(int priority) : IDisposable +public sealed class Props : IDisposable { /// Ordered by lowest to highest priority. Equal priority is disambiguated by unique IDs. /// Note that this is compatible with default object reference equality. @@ -21,7 +21,7 @@ public sealed class Props(int priority) : IDisposable public readonly uint UniqueId = _nextId(); - public readonly int Priority = priority; + public readonly int Priority; private readonly Dictionary props = []; @@ -38,6 +38,12 @@ public sealed class Props(int priority) : IDisposable internal bool NeedsValueUpdate = false; internal bool NeedsEntriesUpdate = false; + public Props(int priority) + { + Priority = priority; + SuppressEagerUpdatesThisFrame(); + } + public void SuppressEagerUpdatesThisFrame() { SuppressEagerUpdate = true; From ed0268b59b4c329ba449d01fd60ec355c8661e97 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 19:15:09 -0400 Subject: [PATCH 19/30] =?UTF-8?q?=F0=9F=A6=86=F0=9F=9A=AB=F0=9F=AA=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MaterialPropertyManager.cs | 21 +++-- Source/DynamicProperties/MpbCompiler.cs | 59 ++++++-------- Source/DynamicProperties/MpbCompilerCache.cs | 52 ++++++++++++ Source/DynamicProperties/Props.cs | 42 ++++++---- Source/DynamicProperties/PropsCascade.cs | 79 +++++++------------ Source/DynamicProperties/README.md | 44 +++++++++++ 6 files changed, 181 insertions(+), 116 deletions(-) create mode 100644 Source/DynamicProperties/MpbCompilerCache.cs create mode 100644 Source/DynamicProperties/README.md diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 0564c7e..7399bec 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -39,7 +39,8 @@ private void OnDestroy() if (Instance != this) return; Instance = null; - PropsCascade.ClearCache(); + foreach (var cascade in rendererCascades.Values) cascade.Dispose(); + MpbCompilerCache.CheckCleared(); // Poor man's GC :'( MaterialColorUpdaterPatch.temperatureColorProps.Clear(); @@ -74,7 +75,9 @@ public bool Remove(Renderer renderer, Props props) public bool Remove(Renderer renderer) { - return rendererCascades.Remove(renderer); + if (!rendererCascades.Remove(renderer, out var cascade)) return false; + cascade.Dispose(); + return true; } public static void RegisterPropertyNamesForDebugLogging(params string[] properties) @@ -85,15 +88,9 @@ public static void RegisterPropertyNamesForDebugLogging(params string[] properti #endregion /// Public API equivalent is calling `Props.Dispose`. - internal bool Remove(Props props) + internal void Remove(Props props) { - var removed = false; - - foreach (var cascade in rendererCascades.Values) removed |= cascade.Remove(props); - - PropsCascade.RemoveCacheEntriesWith(props); - - return removed; + foreach (var cascade in rendererCascades.Values) cascade.Remove(props); } private bool _propRefreshScheduled = false; @@ -105,9 +102,9 @@ private IEnumerator Co_propsLateUpdate() foreach (var props in propsLateUpdateQueue) { if (props.NeedsEntriesUpdate) { - props.OnEntriesChanged(props); + props.OnEntriesChanged?.Invoke(props); } else if (props.NeedsValueUpdate) { - props.OnValueChanged(props, null); + props.OnValueChanged?.Invoke(props, null); } props.SuppressEagerUpdate = diff --git a/Source/DynamicProperties/MpbCompiler.cs b/Source/DynamicProperties/MpbCompiler.cs index bf42538..b6ae4b9 100644 --- a/Source/DynamicProperties/MpbCompiler.cs +++ b/Source/DynamicProperties/MpbCompiler.cs @@ -17,18 +17,18 @@ internal class MpbCompiler : IDisposable private readonly HashSet linkedRenderers = []; private readonly MaterialPropertyBlock mpb = new(); - private readonly Dictionary> idManagerMap = []; + private readonly Dictionary idManagers = []; private static readonly MaterialPropertyBlock EmptyMpb = new(); #endregion - internal MpbCompiler(SortedSet cascades) + internal MpbCompiler(SortedSet cascade) { MaterialPropertyManager.Instance?.LogDebug( - $"new cache entry {RuntimeHelpers.GetHashCode(this)}"); + $"new MpbCompiler instance {RuntimeHelpers.GetHashCode(this)}"); - Cascade = cascades; + Cascade = cascade; RebuildManagerMap(); RewriteMpb(); foreach (var props in Cascade) { @@ -48,16 +48,12 @@ internal void Register(Renderer renderer) internal void Unregister(Renderer renderer) { linkedRenderers.Remove(renderer); - renderer.SetPropertyBlock(EmptyMpb); - CheckLiveness(); - } + if (renderer != null) renderer.SetPropertyBlock(EmptyMpb); - private void CheckLiveness() - { if (linkedRenderers.Count > 0) return; - MaterialPropertyManager.Instance.LogDebug( - $"dead cache entry {RuntimeHelpers.GetHashCode(this)}"); - PropsCascade.RemoveCacheEntry(this); + Log.Debug( + $"last renderer unregistered from MpbCompiler instance {RuntimeHelpers.GetHashCode(this)}"); + MpbCompilerCache.Remove(this); } #endregion @@ -66,22 +62,12 @@ private void CheckLiveness() private void RebuildManagerMap() { - idManagerMap.Clear(); - - Dictionary idManagers = []; + idManagers.Clear(); foreach (var props in Cascade) { foreach (var id in props.ManagedIds) { idManagers[id] = props; } } - - foreach (var (id, props) in idManagers) { - if (!idManagerMap.TryGetValue(props, out var ids)) { - idManagerMap[props] = ids = []; - } - - ids.Add(id); - } } private void OnPropsEntriesChanged(Props props) @@ -105,9 +91,14 @@ private void OnPropsValueChanged(Props props, int? id) private void WriteMpb(Props props, int? id) { if (id.HasValue) { - props.Write(id.GetValueOrDefault(), mpb); + var changedId = id.GetValueOrDefault(); + if (idManagers[changedId] != props) return; + props.Write(changedId, mpb); } else { - foreach (var managedId in idManagerMap[props]) props.Write(managedId, mpb); + foreach (var (managedId, managingProps) in idManagers) { + if (props != managingProps) continue; + props.Write(managedId, mpb); + } } } @@ -115,11 +106,7 @@ private void WriteMpb(Props props, int? id) private void RewriteMpb() { mpb.Clear(); - foreach (var (props, managedIds) in idManagerMap) { - foreach (var managedId in managedIds) { - props.Write(managedId, mpb); - } - } + foreach (var (id, props) in idManagers) props.Write(id, mpb); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -143,7 +130,7 @@ private void ApplyAll() MaterialPropertyManager.Instance.Remove(dead); } - CheckLiveness(); + _deadRenderers.Clear(); } #endregion @@ -152,15 +139,15 @@ private void ApplyAll() private bool _disposed = false; - private void UnlinkProps() + private void HandleDispose() { if (_disposed) return; - Debug.Log("disposing MPB cache entry"); + Log.Debug($"disposing MPB compiler instance {RuntimeHelpers.GetHashCode(this)}"); foreach (var props in Cascade) { - props.OnValueChanged -= OnPropsValueChanged; props.OnEntriesChanged -= OnPropsEntriesChanged; + props.OnValueChanged -= OnPropsValueChanged; } _disposed = true; @@ -168,13 +155,13 @@ private void UnlinkProps() public void Dispose() { - UnlinkProps(); + HandleDispose(); GC.SuppressFinalize(this); } ~MpbCompiler() { - UnlinkProps(); + HandleDispose(); } #endregion diff --git a/Source/DynamicProperties/MpbCompilerCache.cs b/Source/DynamicProperties/MpbCompilerCache.cs new file mode 100644 index 0000000..903a8c0 --- /dev/null +++ b/Source/DynamicProperties/MpbCompilerCache.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using KSPBuildTools; +using UnityEngine; + +namespace Shabby.DynamicProperties; + +internal static class MpbCompilerCache +{ + private static readonly IEqualityComparer> CacheKeyComparer = + SortedSet.CreateSetComparer(); // Object equality is fine. + + private static readonly Dictionary, MpbCompiler> Cache = + new(CacheKeyComparer); + + internal static MpbCompiler Get(SortedSet cascade) + { + if (Cache.TryGetValue(cascade, out var compiler)) { + MaterialPropertyManager.Instance?.LogDebug( + $"MpbCompiler cache hit instance {RuntimeHelpers.GetHashCode(compiler)}"); + return compiler; + } + + // Don't accidentally mutate the cache key... + var clonedCascade = new SortedSet(cascade); + compiler = new MpbCompiler(clonedCascade); +#if DEBUG + if (!(!ReferenceEquals(cascade, compiler.Cascade) && + CacheKeyComparer.Equals(cascade, compiler.Cascade))) { + throw new InvalidOperationException("cache key equality check failed"); + } +#endif + Cache[compiler.Cascade] = compiler; + return compiler; + } + + internal static void Remove(MpbCompiler entry) + { + Cache.Remove(entry.Cascade); + entry.Dispose(); + } + + internal static void CheckCleared() + { + if (Cache.Count == 0) return; + + Debug.LogError($"{Cache.Count} MpbCompilers were not disposed; forcing removal"); + foreach (var compiler in Cache.Values) compiler.Dispose(); + Cache.Clear(); + } +} diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 7c08658..c3cd992 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -6,15 +6,16 @@ namespace Shabby.DynamicProperties; -public sealed class Props : IDisposable +public sealed class Props : IComparable, IDisposable { /// Ordered by lowest to highest priority. Equal priority is disambiguated by unique IDs. - /// Note that this is compatible with default object reference equality. - public static readonly Comparer PriorityComparer = Comparer.Create((a, b) => + public int CompareTo(Props other) { - var priorityCmp = a.Priority.CompareTo(b.Priority); - return priorityCmp != 0 ? priorityCmp : a.UniqueId.CompareTo(b.UniqueId); - }); + if (ReferenceEquals(this, other)) return 0; + if (other is null) return 1; + var priorityCmp = Priority.CompareTo(other.Priority); + return priorityCmp != 0 ? priorityCmp : UniqueId.CompareTo(other.UniqueId); + } private static uint _idCounter = 0; private static uint _nextId() => _idCounter++; @@ -27,16 +28,17 @@ public sealed class Props : IDisposable internal IEnumerable ManagedIds => props.Keys; - internal delegate void ValueChangedHandler(Props props, int? id); - internal delegate void EntriesChangedHandler(Props props); - internal ValueChangedHandler OnValueChanged = delegate { }; internal EntriesChangedHandler OnEntriesChanged = delegate { }; + internal delegate void ValueChangedHandler(Props props, int? id); + + internal ValueChangedHandler OnValueChanged = delegate { }; + internal bool SuppressEagerUpdate = false; - internal bool NeedsValueUpdate = false; internal bool NeedsEntriesUpdate = false; + internal bool NeedsValueUpdate = false; public Props(int priority) { @@ -58,7 +60,7 @@ private void _internalSet(int id, T value) where TProp : Prop if (!typedProp.UpdateIfChanged(value)) return; if (!SuppressEagerUpdate) { - OnValueChanged(this, id); + OnValueChanged?.Invoke(this, id); } else { NeedsValueUpdate = true; } @@ -73,7 +75,7 @@ private void _internalSet(int id, T value) where TProp : Prop props[id] = (TProp)Activator.CreateInstance(typeof(TProp), value); if (!SuppressEagerUpdate) { - OnEntriesChanged(this); + OnEntriesChanged?.Invoke(this); } else { NeedsEntriesUpdate = true; } @@ -126,22 +128,28 @@ public override string ToString() private bool _disposed = false; - private void UnregisterSelf(bool disposing) + private void HandleDispose(bool disposing) { if (_disposed) return; - Debug.Log($"disposing Props instance {UniqueId}"); - if (disposing) MaterialPropertyManager.Instance?.Remove(this); + + if (disposing) { + Log.Debug($"disposing Props instance {UniqueId}"); + MaterialPropertyManager.Instance?.Remove(this); + } else { + Log.Error($"Props instance {UniqueId} was not disposed"); + } + _disposed = true; } public void Dispose() { - UnregisterSelf(true); + HandleDispose(true); GC.SuppressFinalize(this); } ~Props() { - UnregisterSelf(false); + HandleDispose(false); } } diff --git a/Source/DynamicProperties/PropsCascade.cs b/Source/DynamicProperties/PropsCascade.cs index bb22d40..e8e449e 100644 --- a/Source/DynamicProperties/PropsCascade.cs +++ b/Source/DynamicProperties/PropsCascade.cs @@ -2,79 +2,56 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using KSPBuildTools; using UnityEngine; namespace Shabby.DynamicProperties; -internal class PropsCascade(Renderer renderer) +internal class PropsCascade(Renderer renderer) : IDisposable { - internal readonly Renderer Renderer = renderer; + private readonly Renderer renderer = renderer; + private readonly SortedSet cascade = new(); + private MpbCompiler? compiler = null; - private static readonly IEqualityComparer> CacheKeyComparer = - SortedSet.CreateSetComparer(); // Object equality is fine. - - private static readonly Dictionary, MpbCompiler> MpbCache = - new(CacheKeyComparer); - - internal static void ClearCache() => MpbCache.Clear(); - - private readonly SortedSet cascade = new(Props.PriorityComparer); - private MpbCompiler? mpbCompiler = null; - - internal static void RemoveCacheEntry(MpbCompiler entry) + internal bool Add(Props props) { - MpbCache.Remove(entry.Cascade); - entry.Dispose(); - } + if (!cascade.Add(props)) return false; - private static readonly List _entriesToRemove = []; + ReacquireCompiler(); + return true; + } - internal static void RemoveCacheEntriesWith(Props props) + internal bool Remove(Props props) { - foreach (var entry in MpbCache) { - if (entry.Key.Contains(props)) _entriesToRemove.Add(entry.Value); - } + if (!cascade.Remove(props)) return false; - foreach (var entry in _entriesToRemove) RemoveCacheEntry(entry); - _entriesToRemove.Clear(); + ReacquireCompiler(); + return true; } private void ReacquireCompiler() { - mpbCompiler?.Unregister(Renderer); - - if (!MpbCache.TryGetValue(cascade, out mpbCompiler)) { - // Don't accidentally mutate the cache key... - var clonedCascade = new SortedSet(cascade, Props.PriorityComparer); - mpbCompiler = new MpbCompiler(clonedCascade); -#if DEBUG - if (!(!ReferenceEquals(cascade, mpbCompiler.Cascade) && - CacheKeyComparer.Equals(cascade, mpbCompiler.Cascade))) { - throw new InvalidOperationException("cache key equality check failed"); - } -#endif - MpbCache[mpbCompiler.Cascade] = mpbCompiler; - } else { - MaterialPropertyManager.Instance.LogDebug("cache hit"); - } - - mpbCompiler.Register(Renderer); + UnregisterFromCompiler(); + compiler = MpbCompilerCache.Get(cascade); + compiler.Register(renderer); } - internal bool Add(Props props) + private void UnregisterFromCompiler() { - if (!cascade.Add(props)) return false; - - ReacquireCompiler(); - return true; + compiler?.Unregister(renderer); + compiler = null; } - internal bool Remove(Props props) + public void Dispose() { - if (!cascade.Remove(props)) return false; + Log.Debug($"disposing cascade instance {RuntimeHelpers.GetHashCode(this)}"); + UnregisterFromCompiler(); + GC.SuppressFinalize(this); + } - ReacquireCompiler(); - return true; + ~PropsCascade() + { + UnregisterFromCompiler(); } } diff --git a/Source/DynamicProperties/README.md b/Source/DynamicProperties/README.md new file mode 100644 index 0000000..ae6808e --- /dev/null +++ b/Source/DynamicProperties/README.md @@ -0,0 +1,44 @@ +# Dynamic Property Management + +This is a developer-facing API that fully reimplements how the stock code handles +`MaterialPropertyBlock`s (MPBs). + +## Public API + +TODO. + +## Implementation Notes + +The `MaterialPropertyManager` class implements very little behavior. It maintains an association of +registered `Renderer`s to their `Props` instances, stored in `PropsCascade` instances that +facilitate the sorting of the `Props` by priority. + +Upon the addition or removal of a `Props` instance from a `Cascade`, it queries the +`MpbCompilerCache` singleton for a `MpbCompiler` instance linked to the same cascade (lowercase, +_i.e._ a sorted set of `Props`). The renderer managed by the `Cascade` is unregistered from the +previous `Compiler` instance, and registered to the new instance. The previous `Compiler` instance +then checks if it has any remaining linked renderers, and evicts itself from the cache if it has +become unused. + +The `MpbCompiler` maintains a "manager map" of all the property IDs to their managing `Props` +instances, resolved by priority in case of conflict, as well as a single Unity MPB applied to all of +its linked renderers. Upon creation, the `Compiler` registers change handlers to each of its `Props` +instances. There are two types of handlers. When an existing property is changed, the value-changed +handler is fired. This is a fast path, as the managing `Props` of a given ID cannot change. Only +that particular entry of the MPB is updated, and is applied immediately to all linked renderers. +Upon the addition or removal of a property from a `Props`, the entry-changed handler is fired, to +recompute the manager map. The MPB is cleared, repopulated, and reapplied. + +The mod-facing `Props` handles are stored throughout the stack and must be explicitly `Dispose`d. +Upon disposal, `MaterialPropertyManager` removes it from all registered `Cascade`s. This unlinks +each `Cascade` from their `MpbCompiler`s. As all such compilers will reference the disposed `Props`, +they will become dead themselves and be disposed upon unregistration of the last `Cascade`. + +If a renderer is detected to be Unity GCed during MPB application by a `Compiler`, it is removed +from the `MaterialPropertyManager` using the public API. This disposes the associated `Cascade`, and +would dispose the originating `Compiler` instance if its last renderer was unregistered. + +Upon destruction at scene change, `MaterialPropertyManager` disposes all `Cascade`s. This should +clear all `Compiler` cache instances, and is checked to have done so. Any remaining `Props` +instances would be kept alive by external references, and would have been unlinked from their update +handlers. From bfac6a238ae92c5cc7a9368fd5b369442adbd543 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 21:02:28 -0400 Subject: [PATCH 20/30] fix stock patches --- Source/DynamicProperties/MpbCompilerCache.cs | 4 +- .../Patches/MaterialColorUpdaterPatch.cs | 50 +++++++++++++++---- .../Patches/NoDuplicateMaterials.cs | 11 ++-- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/Source/DynamicProperties/MpbCompilerCache.cs b/Source/DynamicProperties/MpbCompilerCache.cs index 903a8c0..663758c 100644 --- a/Source/DynamicProperties/MpbCompilerCache.cs +++ b/Source/DynamicProperties/MpbCompilerCache.cs @@ -1,8 +1,6 @@ -using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using KSPBuildTools; -using UnityEngine; namespace Shabby.DynamicProperties; @@ -45,7 +43,7 @@ internal static void CheckCleared() { if (Cache.Count == 0) return; - Debug.LogError($"{Cache.Count} MpbCompilers were not disposed; forcing removal"); + Log.Error($"{Cache.Count} MpbCompilers were not disposed; forcing removal"); foreach (var compiler in Cache.Values) compiler.Dispose(); Cache.Clear(); } diff --git a/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs b/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs index f0f8d82..79fece0 100644 --- a/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs +++ b/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs @@ -10,19 +10,12 @@ public class MaterialColorUpdaterPatch { internal static readonly Dictionary temperatureColorProps = []; - [HarmonyPostfix] - [HarmonyPatch(MethodType.Constructor, typeof(Transform), typeof(int), typeof(Part))] - private static void MaterialColorUpdater_Ctor_Postfix(MaterialColorUpdater __instance) - { - temperatureColorProps[__instance] = new Props(int.MinValue + 1); - } - [HarmonyPostfix] [HarmonyPatch("CreateRendererList")] private static void MaterialColorUpdater_CreateRendererList_Postfix( MaterialColorUpdater __instance) { - var props = temperatureColorProps[__instance]; + var props = temperatureColorProps[__instance] = new Props(int.MinValue + 1); foreach (var renderer in __instance.renderers) { MaterialPropertyManager.Instance?.Set(renderer, props); } @@ -46,6 +39,7 @@ private static IEnumerable Update_Transpiler( foreach (var insn in insns) { yield return insn; + // this.mpb.SetColor(this.propertyID, this.setColor); // IL_0022: ldarg.0 // this // IL_0023: ldfld class UnityEngine.MaterialPropertyBlock MaterialColorUpdater::mpb // IL_0028: ldarg.0 // this @@ -54,14 +48,17 @@ private static IEnumerable Update_Transpiler( // IL_002f: ldfld valuetype UnityEngine.Color MaterialColorUpdater::setColor // IL_0034: callvirt instance void UnityEngine.MaterialPropertyBlock::SetColor(int32, valuetype UnityEngine.Color) if (insn.Calls(MPB_SetColor)) break; + // Remaining code applies MPB to renderers. } - CodeInstruction[] replace = [ + // MaterialColorUpdaterPatch.Update_SetProperty(this); + // return; + CodeInstruction[] updateProp = [ new(OpCodes.Ldarg_0), // this CodeInstruction.Call(() => Update_SetProperty(default)), new(OpCodes.Ret) ]; - foreach (var insn in replace) yield return insn; + foreach (var insn in updateProp) yield return insn; } private static void DisposeIfExists(MaterialColorUpdater mcu) @@ -84,6 +81,39 @@ private static void Part_OnDestroy_Postfix(Part __instance) DisposeIfExists(__instance.temperatureRenderer); } + [HarmonyTranspiler] + [HarmonyPatch(typeof(ModuleJettison), nameof(ModuleJettison.Jettison))] + private static IEnumerable ModuleJettison_Jettison_Transpiler( + IEnumerable insns) + { + var ModuleJettison_jettisonTemperatureRenderer = AccessTools.Field( + typeof(ModuleJettison), nameof(ModuleJettison.jettisonTemperatureRenderer)); + + // this.jettisonTemperatureRenderer = null; + // IL_0327: ldarg.0 // this + // IL_0328: ldnull + // IL_0329: stfld class MaterialColorUpdater ModuleJettison::jettisonTemperatureRenderer + CodeMatch[] matchSetTempRendererNull = [ + new(OpCodes.Ldarg_0), + new(OpCodes.Ldnull), + new(OpCodes.Stfld, ModuleJettison_jettisonTemperatureRenderer) + ]; + + var matcher = new CodeMatcher(insns); + + matcher + .MatchStartForward(matchSetTempRendererNull) + .ThrowIfNotMatch("failed to find set temp renderer null") + .Insert( + // MaterialColorUpdaterPatch.DisposeIfExists(this.jettisonTemperatureRenderer); + new CodeInstruction(OpCodes.Ldarg_0), // this + new CodeInstruction(OpCodes.Ldfld, ModuleJettison_jettisonTemperatureRenderer), + CodeInstruction.Call(() => DisposeIfExists(default)) + ); + + return matcher.InstructionEnumeration(); + } + // FIXME: write a transpiler for ModuleJettison.Jettison. [HarmonyPostfix] diff --git a/Source/DynamicProperties/Patches/NoDuplicateMaterials.cs b/Source/DynamicProperties/Patches/NoDuplicateMaterials.cs index 1e0736d..a86eae9 100644 --- a/Source/DynamicProperties/Patches/NoDuplicateMaterials.cs +++ b/Source/DynamicProperties/Patches/NoDuplicateMaterials.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics; using System.Reflection; using HarmonyLib; using Highlighting; @@ -48,11 +47,11 @@ internal static class NoDuplicateMaterials private static IEnumerable TargetMethods() => [ AccessTools.Method(typeof(Highlighter), "GrabRenderers"), - AccessTools.Method(typeof(MaterialColorUpdater), "CreateRendererList"), - AccessTools.Method(typeof(ModuleColorChanger), "ProcessMaterialsList"), - AccessTools.Method( - typeof(GameObjectExtension), nameof(GameObjectExtension.SetLayerRecursive), - [typeof(GameObject), typeof(int), typeof(bool), typeof(int)]) + // AccessTools.Method(typeof(MaterialColorUpdater), "CreateRendererList"), + AccessTools.Method(typeof(ModuleColorChanger), "ProcessMaterialsList") + // AccessTools.Method( + // typeof(GameObjectExtension), nameof(GameObjectExtension.SetLayerRecursive), + // [typeof(GameObject), typeof(int), typeof(bool), typeof(int)]) ]; [HarmonyTranspiler] From f5140f68a6b683a741ac588e98bf74ca3f1078ce Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Tue, 10 Jun 2025 21:16:11 -0400 Subject: [PATCH 21/30] make setting props on a null renderer only a warning --- Source/DynamicProperties/MaterialPropertyManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 7399bec..bf32321 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -46,7 +46,7 @@ private void OnDestroy() MaterialColorUpdaterPatch.temperatureColorProps.Clear(); ModuleColorChangerPatch.mccProps.Clear(); - this.LogDebug("destroyed"); + this.LogMessage("destroyed"); } #endregion @@ -56,7 +56,7 @@ private void OnDestroy() public bool Set(Renderer renderer, Props props) { if (renderer == null) { - Log.LogError(this, $"cannot set property on null renderer {renderer.GetHashCode()}"); + this.LogWarning($"cannot set property on null renderer {renderer.GetHashCode()}"); return false; } From 7a67252fb4d200a1bbd5aebc4e9e79670f25b67e Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Wed, 11 Jun 2025 15:40:50 -0400 Subject: [PATCH 22/30] improve destructor logging --- Source/DynamicProperties/Disposable.cs | 39 ++++++++ .../MaterialPropertyManager.cs | 5 +- Source/DynamicProperties/MpbCompiler.cs | 32 +----- Source/DynamicProperties/MpbCompilerCache.cs | 1 + .../Patches/MaterialColorUpdaterPatch.cs | 10 +- .../Patches/ModuleColorChangerPatch.cs | 13 +-- Source/DynamicProperties/Patches/PartPatch.cs | 14 ++- .../DynamicProperties/Patches/StockPatch.cs | 17 ++++ Source/DynamicProperties/Props.cs | 97 +++++++++---------- Source/DynamicProperties/PropsCascade.cs | 17 +--- 10 files changed, 129 insertions(+), 116 deletions(-) create mode 100644 Source/DynamicProperties/Disposable.cs create mode 100644 Source/DynamicProperties/Patches/StockPatch.cs diff --git a/Source/DynamicProperties/Disposable.cs b/Source/DynamicProperties/Disposable.cs new file mode 100644 index 0000000..01800a0 --- /dev/null +++ b/Source/DynamicProperties/Disposable.cs @@ -0,0 +1,39 @@ +using System; +using KSPBuildTools; + +namespace Shabby.DynamicProperties; + +public abstract class Disposable : IDisposable +{ + protected virtual bool IsUnused() => false; + + protected abstract void OnDispose(); + + private bool _disposed = false; + + private void HandleDispose(bool disposing) + { + if (_disposed) return; + + if (disposing) { + Log.Debug($"disposing {GetType().Name} instance {GetHashCode()}"); + OnDispose(); + } else if (!IsUnused()) { + Log.Warning( + $"active {GetType().Name} instance {GetHashCode()} was not disposed"); + } + + _disposed = true; + } + + public void Dispose() + { + HandleDispose(true); + GC.SuppressFinalize(this); + } + + ~Disposable() + { + HandleDispose(false); + } +} diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index bf32321..30e8909 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -43,8 +43,9 @@ private void OnDestroy() MpbCompilerCache.CheckCleared(); // Poor man's GC :'( - MaterialColorUpdaterPatch.temperatureColorProps.Clear(); - ModuleColorChangerPatch.mccProps.Clear(); + PartPatch.ClearOnSceneSwitch(); + MaterialColorUpdaterPatch.ClearOnSceneSwitch(); + ModuleColorChangerPatch.ClearOnSceneSwitch(); this.LogMessage("destroyed"); } diff --git a/Source/DynamicProperties/MpbCompiler.cs b/Source/DynamicProperties/MpbCompiler.cs index b6ae4b9..926bcb0 100644 --- a/Source/DynamicProperties/MpbCompiler.cs +++ b/Source/DynamicProperties/MpbCompiler.cs @@ -1,6 +1,5 @@ #nullable enable -using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using KSPBuildTools; @@ -8,7 +7,7 @@ namespace Shabby.DynamicProperties; -internal class MpbCompiler : IDisposable +internal class MpbCompiler : Disposable { #region Fields @@ -126,8 +125,8 @@ private void ApplyAll() } foreach (var dead in _deadRenderers) { - MaterialPropertyManager.Instance.LogDebug($"dead renderer {dead.GetHashCode()}"); - MaterialPropertyManager.Instance.Remove(dead); + MaterialPropertyManager.Instance?.LogDebug($"dead renderer {dead.GetHashCode()}"); + MaterialPropertyManager.Instance?.Remove(dead); } _deadRenderers.Clear(); @@ -135,34 +134,13 @@ private void ApplyAll() #endregion - #region dtor + protected override bool IsUnused() => linkedRenderers.Count == 0; - private bool _disposed = false; - - private void HandleDispose() + protected override void OnDispose() { - if (_disposed) return; - - Log.Debug($"disposing MPB compiler instance {RuntimeHelpers.GetHashCode(this)}"); - foreach (var props in Cascade) { props.OnEntriesChanged -= OnPropsEntriesChanged; props.OnValueChanged -= OnPropsValueChanged; } - - _disposed = true; - } - - public void Dispose() - { - HandleDispose(); - GC.SuppressFinalize(this); - } - - ~MpbCompiler() - { - HandleDispose(); } - - #endregion } diff --git a/Source/DynamicProperties/MpbCompilerCache.cs b/Source/DynamicProperties/MpbCompilerCache.cs index 663758c..3576592 100644 --- a/Source/DynamicProperties/MpbCompilerCache.cs +++ b/Source/DynamicProperties/MpbCompilerCache.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using KSPBuildTools; diff --git a/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs b/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs index 79fece0..4bd8bb3 100644 --- a/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs +++ b/Source/DynamicProperties/Patches/MaterialColorUpdaterPatch.cs @@ -6,16 +6,14 @@ namespace Shabby.DynamicProperties; [HarmonyPatch(typeof(MaterialColorUpdater))] -public class MaterialColorUpdaterPatch +internal class MaterialColorUpdaterPatch : StockPatchBase { - internal static readonly Dictionary temperatureColorProps = []; - [HarmonyPostfix] [HarmonyPatch("CreateRendererList")] private static void MaterialColorUpdater_CreateRendererList_Postfix( MaterialColorUpdater __instance) { - var props = temperatureColorProps[__instance] = new Props(int.MinValue + 1); + var props = Props[__instance] = new Props(int.MinValue + 1); foreach (var renderer in __instance.renderers) { MaterialPropertyManager.Instance?.Set(renderer, props); } @@ -23,7 +21,7 @@ private static void MaterialColorUpdater_CreateRendererList_Postfix( private static void Update_SetProperty(MaterialColorUpdater mcu) { - temperatureColorProps[mcu].SetColor(mcu.propertyID, mcu.setColor); + Props[mcu].SetColor(mcu.propertyID, mcu.setColor); } [HarmonyTranspiler] @@ -64,7 +62,7 @@ private static IEnumerable Update_Transpiler( private static void DisposeIfExists(MaterialColorUpdater mcu) { if (mcu == null) return; - if (temperatureColorProps.TryGetValue(mcu, out var props)) props.Dispose(); + if (Props.TryGetValue(mcu, out var props)) props.Dispose(); } [HarmonyPrefix] diff --git a/Source/DynamicProperties/Patches/ModuleColorChangerPatch.cs b/Source/DynamicProperties/Patches/ModuleColorChangerPatch.cs index 75ae723..9a8ac24 100644 --- a/Source/DynamicProperties/Patches/ModuleColorChangerPatch.cs +++ b/Source/DynamicProperties/Patches/ModuleColorChangerPatch.cs @@ -1,25 +1,22 @@ -using System.Collections.Generic; using HarmonyLib; namespace Shabby.DynamicProperties; [HarmonyPatch(typeof(ModuleColorChanger))] -internal class ModuleColorChangerPatch +internal class ModuleColorChangerPatch : StockPatchBase { - internal static readonly Dictionary mccProps = []; - [HarmonyPostfix] [HarmonyPatch(nameof(ModuleColorChanger.OnStart))] private static void OnStart_Postfix(ModuleColorChanger __instance) { - mccProps[__instance] = new Props(0); + Props[__instance] = new Props(0); } [HarmonyPostfix] [HarmonyPatch("EditRenderers")] private static void EditRenderers_Postfix(ModuleColorChanger __instance) { - var props = mccProps[__instance]; + var props = Props[__instance]; foreach (var renderer in __instance.renderers) { MaterialPropertyManager.Instance?.Set(renderer, props); } @@ -29,7 +26,7 @@ private static void EditRenderers_Postfix(ModuleColorChanger __instance) [HarmonyPatch("UpdateColor")] public static bool UpdateColor_Prefix(ModuleColorChanger __instance) { - mccProps[__instance].SetColor(__instance.shaderPropertyInt, __instance.color); + Props[__instance].SetColor(__instance.shaderPropertyInt, __instance.color); return false; } @@ -38,7 +35,7 @@ public static bool UpdateColor_Prefix(ModuleColorChanger __instance) private static void Part_OnDestroy_Postfix(Part __instance) { foreach (var mcc in __instance.FindModulesImplementing()) { - if (mccProps.Remove(mcc, out var props)) props.Dispose(); + if (Props.Remove(mcc, out var props)) props.Dispose(); } } diff --git a/Source/DynamicProperties/Patches/PartPatch.cs b/Source/DynamicProperties/Patches/PartPatch.cs index b538d4c..8d98170 100644 --- a/Source/DynamicProperties/Patches/PartPatch.cs +++ b/Source/DynamicProperties/Patches/PartPatch.cs @@ -6,22 +6,20 @@ namespace Shabby.DynamicProperties; [HarmonyPatch(typeof(Part))] -internal static class PartPatch +internal class PartPatch : StockPatchBase { - private static readonly Dictionary rimHighlightProps = []; - [HarmonyPostfix] [HarmonyPatch("Awake")] private static void Awake_Postfix(Part __instance) { - rimHighlightProps[__instance] = new Props(int.MinValue + 1); + Props[__instance] = new Props(int.MinValue + 1); } [HarmonyPostfix] [HarmonyPatch("CreateRendererLists")] private static void CreateRendererLists_Postfix(Part __instance) { - var props = rimHighlightProps[__instance]; + var props = Props[__instance]; props.SetFloat(PropertyIDs._RimFalloff, 2f); props.SetColor(PropertyIDs._RimColor, Part.defaultHighlightNone); foreach (var renderer in __instance.HighlightRenderer) { @@ -35,13 +33,13 @@ private static bool SetOpacity_Prefix(Part __instance, float opacity) { __instance.CreateRendererLists(); __instance.mpb.SetFloat(PropertyIDs._Opacity, opacity); - rimHighlightProps[__instance].SetFloat(PropertyIDs._Opacity, opacity); + Props[__instance].SetFloat(PropertyIDs._Opacity, opacity); return false; } private static void Highlight_SetRimColor(Part part, Color color) { - rimHighlightProps[part].SetColor(PropertyIDs._RimColor, color); + Props[part].SetColor(PropertyIDs._RimColor, color); } [HarmonyTranspiler] @@ -122,7 +120,7 @@ private static IEnumerable Highlight_Transpiler( [HarmonyPatch("OnDestroy")] private static void OnDestroy_Postfix(Part __instance) { - if (rimHighlightProps.Remove(__instance, out var props)) { + if (Props.Remove(__instance, out var props)) { props.Dispose(); } } diff --git a/Source/DynamicProperties/Patches/StockPatch.cs b/Source/DynamicProperties/Patches/StockPatch.cs new file mode 100644 index 0000000..ce04081 --- /dev/null +++ b/Source/DynamicProperties/Patches/StockPatch.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using KSPBuildTools; + +namespace Shabby.DynamicProperties; + +internal abstract class StockPatchBase +{ + internal static readonly Dictionary Props = []; + + internal static void ClearOnSceneSwitch() + { + if (Props.Count == 0) return; + + Log.Message($"cleared {Props.Count} Props instances", $"[{typeof(T).Name} MPM Patch]"); + Props.Clear(); + } +} diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index c3cd992..ff9aa10 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -6,16 +6,9 @@ namespace Shabby.DynamicProperties; -public sealed class Props : IComparable, IDisposable +public sealed class Props : Disposable, IComparable { - /// Ordered by lowest to highest priority. Equal priority is disambiguated by unique IDs. - public int CompareTo(Props other) - { - if (ReferenceEquals(this, other)) return 0; - if (other is null) return 1; - var priorityCmp = Priority.CompareTo(other.Priority); - return priorityCmp != 0 ? priorityCmp : UniqueId.CompareTo(other.UniqueId); - } + #region Fields private static uint _idCounter = 0; private static uint _nextId() => _idCounter++; @@ -30,20 +23,54 @@ public int CompareTo(Props other) internal delegate void EntriesChangedHandler(Props props); - internal EntriesChangedHandler OnEntriesChanged = delegate { }; + internal EntriesChangedHandler OnEntriesChanged = null; internal delegate void ValueChangedHandler(Props props, int? id); - internal ValueChangedHandler OnValueChanged = delegate { }; + internal ValueChangedHandler OnValueChanged = null; internal bool SuppressEagerUpdate = false; internal bool NeedsEntriesUpdate = false; internal bool NeedsValueUpdate = false; + #endregion + public Props(int priority) { Priority = priority; SuppressEagerUpdatesThisFrame(); + Log.Debug($"new Props instance {UniqueId}"); + } + + /// Ordered by lowest to highest priority. Equal priority is disambiguated by unique IDs. + public int CompareTo(Props other) + { + if (ReferenceEquals(this, other)) return 0; + if (other is null) return 1; + var priorityCmp = Priority.CompareTo(other.Priority); + return priorityCmp != 0 ? priorityCmp : UniqueId.CompareTo(other.UniqueId); + } + + /// This is equivalent to reference equality. + public override int GetHashCode() => unchecked((int)UniqueId); + + public override string ToString() + { + var sb = StringBuilderCache.Acquire(); + sb.AppendFormat("(Priority {0}) {{\n", Priority); + foreach (var (id, prop) in props) { + sb.AppendFormat("{0} = {1}\n", PropIdToName.Get(id), prop); + } + + sb.AppendLine("}"); + return sb.ToStringAndRelease(); + } + + #region Set/Remove + + private bool HasConsumer() + { + return OnValueChanged.GetInvocationList().Length > 0; } public void SuppressEagerUpdatesThisFrame() @@ -68,7 +95,7 @@ private void _internalSet(int id, T value) where TProp : Prop return; } - MaterialPropertyManager.Instance.LogWarning( + MaterialPropertyManager.Instance?.LogWarning( $"property {PropIdToName.Get(id)} has mismatched type; overwriting with {typeof(T).Name}!"); } @@ -93,6 +120,10 @@ private void _internalSet(int id, T value) where TProp : Prop public void SetTexture(int id, Texture value) => _internalSet(id, value); public void SetVector(int id, Vector4 value) => _internalSet(id, value); + #endregion + + #region Has + private bool _internalHas(int id) => props.TryGetValue(id, out var prop) && prop is Prop; public bool HasColor(int id) => _internalHas(id); @@ -101,6 +132,8 @@ private void _internalSet(int id, T value) where TProp : Prop public bool HasTexture(int id) => _internalHas(id); public bool HasVector(int id) => _internalHas(id); + #endregion + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Write(int id, MaterialPropertyBlock mpb) { @@ -114,42 +147,6 @@ internal void Write(int id, MaterialPropertyBlock mpb) prop.Write(id, mpb); } - public override string ToString() - { - var sb = StringBuilderCache.Acquire(); - sb.AppendFormat("(Priority {0}) {{\n", Priority); - foreach (var (id, prop) in props) { - sb.AppendFormat("{0} = {1}\n", PropIdToName.Get(id), prop); - } - - sb.AppendLine("}"); - return sb.ToStringAndRelease(); - } - - private bool _disposed = false; - - private void HandleDispose(bool disposing) - { - if (_disposed) return; - - if (disposing) { - Log.Debug($"disposing Props instance {UniqueId}"); - MaterialPropertyManager.Instance?.Remove(this); - } else { - Log.Error($"Props instance {UniqueId} was not disposed"); - } - - _disposed = true; - } - - public void Dispose() - { - HandleDispose(true); - GC.SuppressFinalize(this); - } - - ~Props() - { - HandleDispose(false); - } + protected override bool IsUnused() => OnEntriesChanged == null && OnValueChanged == null; + protected override void OnDispose() => MaterialPropertyManager.Instance?.Remove(this); } diff --git a/Source/DynamicProperties/PropsCascade.cs b/Source/DynamicProperties/PropsCascade.cs index e8e449e..f058406 100644 --- a/Source/DynamicProperties/PropsCascade.cs +++ b/Source/DynamicProperties/PropsCascade.cs @@ -1,14 +1,11 @@ #nullable enable -using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; -using KSPBuildTools; using UnityEngine; namespace Shabby.DynamicProperties; -internal class PropsCascade(Renderer renderer) : IDisposable +internal class PropsCascade(Renderer renderer) : Disposable { private readonly Renderer renderer = renderer; private readonly SortedSet cascade = new(); @@ -43,15 +40,5 @@ private void UnregisterFromCompiler() compiler = null; } - public void Dispose() - { - Log.Debug($"disposing cascade instance {RuntimeHelpers.GetHashCode(this)}"); - UnregisterFromCompiler(); - GC.SuppressFinalize(this); - } - - ~PropsCascade() - { - UnregisterFromCompiler(); - } + protected override void OnDispose() => UnregisterFromCompiler(); } From bfd97d67108f0c06951920c1a5b52b266833cbb4 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Wed, 11 Jun 2025 16:57:37 -0400 Subject: [PATCH 23/30] add a remove API to Props --- Source/DynamicProperties/Props.cs | 52 ++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index ff9aa10..d29559e 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -68,30 +68,39 @@ public override string ToString() #region Set/Remove - private bool HasConsumer() - { - return OnValueChanged.GetInvocationList().Length > 0; - } - public void SuppressEagerUpdatesThisFrame() { SuppressEagerUpdate = true; MaterialPropertyManager.Instance?.ScheduleLateUpdate(this); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FireOnEntriesChanged() + { + if (!SuppressEagerUpdate) { + OnEntriesChanged?.Invoke(this); + } else { + NeedsEntriesUpdate = true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FireOnValueChanged(int? id) + { + if (!SuppressEagerUpdate) { + OnValueChanged?.Invoke(this, id); + } else { + NeedsValueUpdate = true; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void _internalSet(int id, T value) where TProp : Prop { if (props.TryGetValue(id, out var prop)) { if (prop is TProp typedProp) { if (!typedProp.UpdateIfChanged(value)) return; - - if (!SuppressEagerUpdate) { - OnValueChanged?.Invoke(this, id); - } else { - NeedsValueUpdate = true; - } - + FireOnValueChanged(id); return; } @@ -100,12 +109,7 @@ private void _internalSet(int id, T value) where TProp : Prop } props[id] = (TProp)Activator.CreateInstance(typeof(TProp), value); - - if (!SuppressEagerUpdate) { - OnEntriesChanged?.Invoke(this); - } else { - NeedsEntriesUpdate = true; - } + FireOnEntriesChanged(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -117,13 +121,25 @@ private void _internalSet(int id, T value) where TProp : Prop [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetInt(int id, int value) => _internalSet(id, value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetTexture(int id, Texture value) => _internalSet(id, value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetVector(int id, Vector4 value) => _internalSet(id, value); + public bool Remove(int id) + { + var removed = props.Remove(id); + if (!removed) return false; + FireOnEntriesChanged(); + return true; + } + #endregion #region Has + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool _internalHas(int id) => props.TryGetValue(id, out var prop) && prop is Prop; public bool HasColor(int id) => _internalHas(id); From 93c78d400c62c44d4f10f49fb07c96e066270e5c Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sat, 14 Jun 2025 14:55:13 -0400 Subject: [PATCH 24/30] patch stock fairings --- .../MaterialPropertyManager.cs | 1 + .../Patches/FairingPanelPatch.cs | 38 +++++++++++++++++++ Source/Shabby.csproj | 3 ++ 3 files changed, 42 insertions(+) create mode 100644 Source/DynamicProperties/Patches/FairingPanelPatch.cs diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 30e8909..e377cb2 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -46,6 +46,7 @@ private void OnDestroy() PartPatch.ClearOnSceneSwitch(); MaterialColorUpdaterPatch.ClearOnSceneSwitch(); ModuleColorChangerPatch.ClearOnSceneSwitch(); + FairingPanelPatch.ClearOnSceneSwitch(); this.LogMessage("destroyed"); } diff --git a/Source/DynamicProperties/Patches/FairingPanelPatch.cs b/Source/DynamicProperties/Patches/FairingPanelPatch.cs new file mode 100644 index 0000000..92505ad --- /dev/null +++ b/Source/DynamicProperties/Patches/FairingPanelPatch.cs @@ -0,0 +1,38 @@ +using HarmonyLib; +using ProceduralFairings; + +namespace Shabby.DynamicProperties; + +[HarmonyPatch(typeof(FairingPanel))] +internal class FairingPanelPatch : StockPatchBase +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(FairingPanel.SetOpacity))] + private static bool SetOpacity_Transpiler(FairingPanel __instance, float o) + { + __instance.opacity = o; + + if (!Props.TryGetValue(__instance, out var props)) { + props = Props[__instance] = new Props(0); + MaterialPropertyManager.Instance?.Set(__instance.mr, props); + if (__instance.attachedFlagParts is { Count: > 0 }) { + foreach (var flagPart in __instance.attachedFlagParts) { + foreach (var flagRenderer in flagPart.flagMeshRenderers) { + MaterialPropertyManager.Instance?.Set(flagRenderer, props); + } + } + } + } + + props.SetFloat(PropertyIDs._Opacity, o); + + return false; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(FairingPanel.Despawn))] + private static void FairingPanel_Despawn(FairingPanel __instance) + { + if (Props.Remove(__instance, out var props)) props.Dispose(); + } +} diff --git a/Source/Shabby.csproj b/Source/Shabby.csproj index a584f93..e7d61f7 100644 --- a/Source/Shabby.csproj +++ b/Source/Shabby.csproj @@ -51,6 +51,9 @@ + + + From 79cbd89f8e91a11e29c312c8930ac6ec8c44c933 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sat, 14 Jun 2025 15:51:10 -0400 Subject: [PATCH 25/30] check for null renderers on all operations --- .../MaterialPropertyManager.cs | 40 +++++++++++++++---- Source/DynamicProperties/MpbCompiler.cs | 20 ++++------ Source/DynamicProperties/Props.cs | 2 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index e377cb2..f63c63c 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -57,10 +57,7 @@ private void OnDestroy() public bool Set(Renderer renderer, Props props) { - if (renderer == null) { - this.LogWarning($"cannot set property on null renderer {renderer.GetHashCode()}"); - return false; - } + if (!CheckRendererAlive(renderer)) return false; if (!rendererCascades.TryGetValue(renderer, out var cascade)) { rendererCascades[renderer] = cascade = new PropsCascade(renderer); @@ -69,15 +66,18 @@ public bool Set(Renderer renderer, Props props) return cascade.Add(props); } - public bool Remove(Renderer renderer, Props props) + public bool Unset(Renderer renderer, Props props) { + if (!CheckRendererAlive(renderer)) return false; if (!rendererCascades.TryGetValue(renderer, out var cascade)) return false; return cascade.Remove(props); } - public bool Remove(Renderer renderer) + public bool Unregister(Renderer renderer) { + if ((object)renderer == null) return false; if (!rendererCascades.Remove(renderer, out var cascade)) return false; + if (renderer == null) this.LogDebug($"dead renderer {renderer.GetHashCode()}"); cascade.Dispose(); return true; } @@ -89,10 +89,34 @@ public static void RegisterPropertyNamesForDebugLogging(params string[] properti #endregion + private bool CheckRendererAlive(Renderer renderer) + { + if (renderer != null) return true; + this.LogWarning($"cannot modify null renderer {renderer?.GetHashCode()}"); + if ((object)renderer != null) Unregister(renderer); + return false; + } + + private readonly List _deadRenderers = []; + + internal void CheckRemoveDeadRenderers() + { + foreach (var renderer in rendererCascades.Keys) { + if (renderer == null) _deadRenderers.Add(renderer); + } + + foreach (var deadRenderer in _deadRenderers) Unregister(deadRenderer); + _deadRenderers.Clear(); + } + /// Public API equivalent is calling `Props.Dispose`. - internal void Remove(Props props) + internal void Unregister(Props props) { - foreach (var cascade in rendererCascades.Values) cascade.Remove(props); + foreach (var (renderer, cascade) in rendererCascades) { + if (renderer != null) cascade.Remove(props); + } + + CheckRemoveDeadRenderers(); } private bool _propRefreshScheduled = false; diff --git a/Source/DynamicProperties/MpbCompiler.cs b/Source/DynamicProperties/MpbCompiler.cs index 926bcb0..e042bc2 100644 --- a/Source/DynamicProperties/MpbCompiler.cs +++ b/Source/DynamicProperties/MpbCompiler.cs @@ -111,25 +111,19 @@ private void RewriteMpb() [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Apply(Renderer renderer) => renderer.SetPropertyBlock(mpb); - private readonly List _deadRenderers = []; - private void ApplyAll() { + var hasDeadRenderer = false; + foreach (var renderer in linkedRenderers) { - if (renderer == null) { - _deadRenderers.Add(renderer!); - continue; + if (renderer != null) { + Apply(renderer); + } else { + hasDeadRenderer = true; } - - Apply(renderer); - } - - foreach (var dead in _deadRenderers) { - MaterialPropertyManager.Instance?.LogDebug($"dead renderer {dead.GetHashCode()}"); - MaterialPropertyManager.Instance?.Remove(dead); } - _deadRenderers.Clear(); + if (hasDeadRenderer) MaterialPropertyManager.Instance?.CheckRemoveDeadRenderers(); } #endregion diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index d29559e..40496ce 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -164,5 +164,5 @@ internal void Write(int id, MaterialPropertyBlock mpb) } protected override bool IsUnused() => OnEntriesChanged == null && OnValueChanged == null; - protected override void OnDispose() => MaterialPropertyManager.Instance?.Remove(this); + protected override void OnDispose() => MaterialPropertyManager.Instance?.Unregister(this); } From 08a52e7490b56520a4183c0374e29230e00a3272 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 15 Jun 2025 16:51:46 -0400 Subject: [PATCH 26/30] Helper methods for checking if Unity object is destroyed --- .../MaterialPropertyManager.cs | 46 +++++++++++-------- Source/DynamicProperties/MpbCompiler.cs | 12 ++--- Source/DynamicProperties/Utils.cs | 6 +++ Source/Shabby.csproj | 15 ++---- 4 files changed, 43 insertions(+), 36 deletions(-) diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index f63c63c..3f127ec 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -1,3 +1,5 @@ +#nullable enable + using System.Collections.Generic; using KSPBuildTools; using UnityEngine; @@ -9,7 +11,7 @@ public sealed class MaterialPropertyManager : MonoBehaviour { #region Fields - public static MaterialPropertyManager Instance { get; private set; } + public static MaterialPropertyManager? Instance { get; private set; } private readonly Dictionary rendererCascades = []; @@ -75,9 +77,9 @@ public bool Unset(Renderer renderer, Props props) public bool Unregister(Renderer renderer) { - if ((object)renderer == null) return false; + if (renderer.IsNullref()) return false; if (!rendererCascades.Remove(renderer, out var cascade)) return false; - if (renderer == null) this.LogDebug($"dead renderer {renderer.GetHashCode()}"); + if (renderer.IsDestroyed()) this.LogDebug($"destroyed renderer {renderer.GetHashCode()}"); cascade.Dispose(); return true; } @@ -91,35 +93,43 @@ public static void RegisterPropertyNamesForDebugLogging(params string[] properti private bool CheckRendererAlive(Renderer renderer) { - if (renderer != null) return true; - this.LogWarning($"cannot modify null renderer {renderer?.GetHashCode()}"); - if ((object)renderer != null) Unregister(renderer); - return false; + if (renderer.IsNullref()) { + Log.LogError(this, "renderer reference is null"); + return false; + } + + if (renderer.IsDestroyed()) { + this.LogWarning($"cannot modify destroyed renderer {renderer.GetHashCode()}"); + Unregister(renderer); + return false; + } + + return true; } - private readonly List _deadRenderers = []; + private readonly List _destroyedRenderers = []; - internal void CheckRemoveDeadRenderers() + internal void CheckRemoveDestroyedRenderers() { foreach (var renderer in rendererCascades.Keys) { - if (renderer == null) _deadRenderers.Add(renderer); + if (renderer.IsDestroyed()) _destroyedRenderers.Add(renderer); } - foreach (var deadRenderer in _deadRenderers) Unregister(deadRenderer); - _deadRenderers.Clear(); + foreach (var destroyed in _destroyedRenderers) Unregister(destroyed); + _destroyedRenderers.Clear(); } /// Public API equivalent is calling `Props.Dispose`. internal void Unregister(Props props) { foreach (var (renderer, cascade) in rendererCascades) { - if (renderer != null) cascade.Remove(props); + if (!renderer.IsDestroyed()) cascade.Remove(props); } - CheckRemoveDeadRenderers(); + CheckRemoveDestroyedRenderers(); } - private bool _propRefreshScheduled = false; + private bool _propsUpdateScheduled = false; private static readonly WaitForEndOfFrame WfEoF = new(); private IEnumerator Co_propsLateUpdate() @@ -138,14 +148,14 @@ private IEnumerator Co_propsLateUpdate() } propsLateUpdateQueue.Clear(); - _propRefreshScheduled = false; + _propsUpdateScheduled = false; } internal void ScheduleLateUpdate(Props props) { propsLateUpdateQueue.Add(props); - if (_propRefreshScheduled) return; + if (_propsUpdateScheduled) return; StartCoroutine(Co_propsLateUpdate()); - _propRefreshScheduled = true; + _propsUpdateScheduled = true; } } diff --git a/Source/DynamicProperties/MpbCompiler.cs b/Source/DynamicProperties/MpbCompiler.cs index e042bc2..e954105 100644 --- a/Source/DynamicProperties/MpbCompiler.cs +++ b/Source/DynamicProperties/MpbCompiler.cs @@ -47,7 +47,7 @@ internal void Register(Renderer renderer) internal void Unregister(Renderer renderer) { linkedRenderers.Remove(renderer); - if (renderer != null) renderer.SetPropertyBlock(EmptyMpb); + if (!renderer.IsDestroyed()) renderer.SetPropertyBlock(EmptyMpb); if (linkedRenderers.Count > 0) return; Log.Debug( @@ -113,17 +113,17 @@ private void RewriteMpb() private void ApplyAll() { - var hasDeadRenderer = false; + var hasDestroyedRenderer = false; foreach (var renderer in linkedRenderers) { - if (renderer != null) { - Apply(renderer); + if (renderer.IsDestroyed()) { + hasDestroyedRenderer = true; } else { - hasDeadRenderer = true; + Apply(renderer); } } - if (hasDeadRenderer) MaterialPropertyManager.Instance?.CheckRemoveDeadRenderers(); + if (hasDestroyedRenderer) MaterialPropertyManager.Instance?.CheckRemoveDestroyedRenderers(); } #endregion diff --git a/Source/DynamicProperties/Utils.cs b/Source/DynamicProperties/Utils.cs index 204ee69..e8ddf01 100644 --- a/Source/DynamicProperties/Utils.cs +++ b/Source/DynamicProperties/Utils.cs @@ -6,6 +6,12 @@ namespace Shabby.DynamicProperties; public static class Utils { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDestroyed(this UnityEngine.Object obj) => obj.m_CachedPtr == IntPtr.Zero; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNullref(this UnityEngine.Object obj) => ReferenceEquals(obj, null); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ApproxEqualsAbs(float a, float b, float eps) => Math.Abs(b - a) <= eps; diff --git a/Source/Shabby.csproj b/Source/Shabby.csproj index e7d61f7..57adc86 100644 --- a/Source/Shabby.csproj +++ b/Source/Shabby.csproj @@ -7,7 +7,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers @@ -43,17 +43,8 @@ - - - - - - - - - - - + + From 7fa885beed209b51462df1b0834035cb1e2f0426 Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 15 Jun 2025 17:26:02 -0400 Subject: [PATCH 27/30] Get methods for props --- Source/DynamicProperties/Props.cs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Source/DynamicProperties/Props.cs b/Source/DynamicProperties/Props.cs index 40496ce..6b4c3b8 100644 --- a/Source/DynamicProperties/Props.cs +++ b/Source/DynamicProperties/Props.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -13,9 +15,9 @@ public sealed class Props : Disposable, IComparable private static uint _idCounter = 0; private static uint _nextId() => _idCounter++; - public readonly uint UniqueId = _nextId(); + public uint UniqueId { get; } = _nextId(); - public readonly int Priority; + public int Priority { get; } private readonly Dictionary props = []; @@ -23,11 +25,11 @@ public sealed class Props : Disposable, IComparable internal delegate void EntriesChangedHandler(Props props); - internal EntriesChangedHandler OnEntriesChanged = null; + internal EntriesChangedHandler? OnEntriesChanged = null; internal delegate void ValueChangedHandler(Props props, int? id); - internal ValueChangedHandler OnValueChanged = null; + internal ValueChangedHandler? OnValueChanged = null; internal bool SuppressEagerUpdate = false; internal bool NeedsEntriesUpdate = false; @@ -43,10 +45,10 @@ public Props(int priority) } /// Ordered by lowest to highest priority. Equal priority is disambiguated by unique IDs. - public int CompareTo(Props other) + public int CompareTo(Props? other) { if (ReferenceEquals(this, other)) return 0; - if (other is null) return 1; + if (other == null) return 1; var priorityCmp = Priority.CompareTo(other.Priority); return priorityCmp != 0 ? priorityCmp : UniqueId.CompareTo(other.UniqueId); } @@ -137,7 +139,7 @@ public bool Remove(int id) #endregion - #region Has + #region Has/Get [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool _internalHas(int id) => props.TryGetValue(id, out var prop) && prop is Prop; @@ -148,6 +150,18 @@ public bool Remove(int id) public bool HasTexture(int id) => _internalHas(id); public bool HasVector(int id) => _internalHas(id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private T? _internalGet(int id) where TProp : Prop => + props.TryGetValue(id, out var prop) && prop is TProp typedProp + ? typedProp.Value + : default; + + public Color GetColorOrDefault(int id) => _internalGet(id); + public float GetFloatOrDefault(int id) => _internalGet(id); + public int GetIntOrDefault(int id) => _internalGet(id); + public Texture? GetTextureOrDefault(int id) => _internalGet(id); + public Vector4 GetVectorOrDefault(int id) => _internalGet(id); + #endregion [MethodImpl(MethodImplOptions.AggressiveInlining)] From 4c74786c12c7aff90045ef0fb2007c2aa19e32ad Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 15 Jun 2025 17:27:36 -0400 Subject: [PATCH 28/30] API to acquire the props instance containing stock properties --- Source/DynamicProperties/MaterialPropertyManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index 3f127ec..c59d80e 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -84,6 +84,11 @@ public bool Unregister(Renderer renderer) return true; } + /// Get a reference to the `Props` instance containing the stock properties of the given + /// `part` (namely, `_Opacity`, `_RimFalloff`, `_RimColor`, and `_TemperatureColor` (flight + /// only)). The returned instance must not be written to. + public Props? GetStockPropsForPart(Part part) => PartPatch.Props.GetValueOrDefault(part); + public static void RegisterPropertyNamesForDebugLogging(params string[] properties) { foreach (var property in properties) PropIdToName.Register(property); From fa301e8f13bcde4ab83e5c83884c8e23e75a10eb Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 15 Jun 2025 18:04:28 -0400 Subject: [PATCH 29/30] keep the rim color intact in the part mpb --- Source/DynamicProperties/Patches/PartPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/DynamicProperties/Patches/PartPatch.cs b/Source/DynamicProperties/Patches/PartPatch.cs index 8d98170..cb9fbfc 100644 --- a/Source/DynamicProperties/Patches/PartPatch.cs +++ b/Source/DynamicProperties/Patches/PartPatch.cs @@ -102,7 +102,7 @@ private static IEnumerable Highlight_Transpiler( .Start() .MatchStartForward(matchSetRimColor) .ThrowIfNotMatch("failed to find MPB set _RimColor call") - .RemoveInstructions(matchSetRimColor.Length) + .Advance(matchSetRimColor.Length) .InsertAndAdvance( // PartPatch.Highlight_SetRimColor(this, value); new CodeInstruction(OpCodes.Ldarg_0), // `this` From dbf4bc2845a93c862a91e8d4f59a1daba7f44f0c Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Fri, 20 Jun 2025 18:36:51 -0400 Subject: [PATCH 30/30] the temp color is stored separately... --- .../MaterialPropertyManager.cs | 27 +++++++++++++------ Source/DynamicProperties/MpbCompiler.cs | 12 ++++----- .../DynamicProperties/Patches/StockPatch.cs | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Source/DynamicProperties/MaterialPropertyManager.cs b/Source/DynamicProperties/MaterialPropertyManager.cs index c59d80e..40392c2 100644 --- a/Source/DynamicProperties/MaterialPropertyManager.cs +++ b/Source/DynamicProperties/MaterialPropertyManager.cs @@ -45,10 +45,10 @@ private void OnDestroy() MpbCompilerCache.CheckCleared(); // Poor man's GC :'( - PartPatch.ClearOnSceneSwitch(); - MaterialColorUpdaterPatch.ClearOnSceneSwitch(); - ModuleColorChangerPatch.ClearOnSceneSwitch(); - FairingPanelPatch.ClearOnSceneSwitch(); + PartPatch.CheckCleared(); + MaterialColorUpdaterPatch.CheckCleared(); + ModuleColorChangerPatch.CheckCleared(); + FairingPanelPatch.CheckCleared(); this.LogMessage("destroyed"); } @@ -85,10 +85,21 @@ public bool Unregister(Renderer renderer) } /// Get a reference to the `Props` instance containing the stock properties of the given - /// `part` (namely, `_Opacity`, `_RimFalloff`, `_RimColor`, and `_TemperatureColor` (flight - /// only)). The returned instance must not be written to. + /// `part` (namely, `_Opacity`, `_RimFalloff`, and `_RimColor`). + /// The returned instance must not be written to. public Props? GetStockPropsForPart(Part part) => PartPatch.Props.GetValueOrDefault(part); + /// Get the part's current `_TemperatureColor` property, if it is set (only in flight). + public Color? GetStockTemperatureColorForPart(Part part) + { + if (!MaterialColorUpdaterPatch.Props.TryGetValue(part.temperatureRenderer, out var props)) { + return null; + } + + if (!props.HasColor(PhysicsGlobals.temperaturePropertyID)) return null; + return props.GetColorOrDefault(PhysicsGlobals.temperaturePropertyID); + } + public static void RegisterPropertyNamesForDebugLogging(params string[] properties) { foreach (var property in properties) PropIdToName.Register(property); @@ -137,7 +148,7 @@ internal void Unregister(Props props) private bool _propsUpdateScheduled = false; private static readonly WaitForEndOfFrame WfEoF = new(); - private IEnumerator Co_propsLateUpdate() + private IEnumerator Co_PropsLateUpdate() { yield return WfEoF; @@ -160,7 +171,7 @@ internal void ScheduleLateUpdate(Props props) { propsLateUpdateQueue.Add(props); if (_propsUpdateScheduled) return; - StartCoroutine(Co_propsLateUpdate()); + StartCoroutine(Co_PropsLateUpdate()); _propsUpdateScheduled = true; } } diff --git a/Source/DynamicProperties/MpbCompiler.cs b/Source/DynamicProperties/MpbCompiler.cs index e954105..152ee71 100644 --- a/Source/DynamicProperties/MpbCompiler.cs +++ b/Source/DynamicProperties/MpbCompiler.cs @@ -14,7 +14,7 @@ internal class MpbCompiler : Disposable /// Immutable. internal readonly SortedSet Cascade; - private readonly HashSet linkedRenderers = []; + private readonly HashSet managedRenderers = []; private readonly MaterialPropertyBlock mpb = new(); private readonly Dictionary idManagers = []; @@ -40,16 +40,16 @@ internal MpbCompiler(SortedSet cascade) internal void Register(Renderer renderer) { - linkedRenderers.Add(renderer); + managedRenderers.Add(renderer); Apply(renderer); } internal void Unregister(Renderer renderer) { - linkedRenderers.Remove(renderer); + managedRenderers.Remove(renderer); if (!renderer.IsDestroyed()) renderer.SetPropertyBlock(EmptyMpb); - if (linkedRenderers.Count > 0) return; + if (managedRenderers.Count > 0) return; Log.Debug( $"last renderer unregistered from MpbCompiler instance {RuntimeHelpers.GetHashCode(this)}"); MpbCompilerCache.Remove(this); @@ -115,7 +115,7 @@ private void ApplyAll() { var hasDestroyedRenderer = false; - foreach (var renderer in linkedRenderers) { + foreach (var renderer in managedRenderers) { if (renderer.IsDestroyed()) { hasDestroyedRenderer = true; } else { @@ -128,7 +128,7 @@ private void ApplyAll() #endregion - protected override bool IsUnused() => linkedRenderers.Count == 0; + protected override bool IsUnused() => managedRenderers.Count == 0; protected override void OnDispose() { diff --git a/Source/DynamicProperties/Patches/StockPatch.cs b/Source/DynamicProperties/Patches/StockPatch.cs index ce04081..3cd85d6 100644 --- a/Source/DynamicProperties/Patches/StockPatch.cs +++ b/Source/DynamicProperties/Patches/StockPatch.cs @@ -7,7 +7,7 @@ internal abstract class StockPatchBase { internal static readonly Dictionary Props = []; - internal static void ClearOnSceneSwitch() + internal static void CheckCleared() { if (Props.Count == 0) return;