Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9986918
kinda working MPB management?
al2me6 Jun 7, 2025
ac616a7
better change tracking, and don't mutate the cache key...
al2me6 Jun 8, 2025
06c5e21
a best-effort attempt at recovering property names for diagnostics
al2me6 Jun 8, 2025
bbd62ec
just use dynamic dispatch
al2me6 Jun 8, 2025
4c86094
patch Part::Highlight
al2me6 Jun 8, 2025
4374883
put the property manager in a namespace
al2me6 Jun 8, 2025
3127597
add a remove method to the manager
al2me6 Jun 8, 2025
c46d585
switch to KSPAddon
al2me6 Jun 9, 2025
b05c785
rewrite it again
al2me6 Jun 10, 2025
4a4a335
add a mechanism to suppress eager updates
al2me6 Jun 10, 2025
d82b205
use approx equality for prop value updates
al2me6 Jun 10, 2025
ce983c4
check dead cache entry more eagerly
al2me6 Jun 10, 2025
25f189c
fix NRE
al2me6 Jun 10, 2025
c23ba51
begin patching MCC and MCU
al2me6 Jun 10, 2025
11fdd97
move Prop to a separate file
al2me6 Jun 10, 2025
0ad8a66
add a public API for registering debug id => name entries
al2me6 Jun 10, 2025
bcf5c77
per-id differencing
al2me6 Jun 10, 2025
bcd6b0e
suppress eager updates on Props creation
al2me6 Jun 10, 2025
ed0268b
🦆🚫🪲
al2me6 Jun 10, 2025
bfac6a2
fix stock patches
al2me6 Jun 11, 2025
f5140f6
make setting props on a null renderer only a warning
al2me6 Jun 11, 2025
7a67252
improve destructor logging
al2me6 Jun 11, 2025
bfd97d6
add a remove API to Props
al2me6 Jun 11, 2025
93c78d4
patch stock fairings
al2me6 Jun 14, 2025
79cbd89
check for null renderers on all operations
al2me6 Jun 14, 2025
08a52e7
Helper methods for checking if Unity object is destroyed
al2me6 Jun 15, 2025
7fa885b
Get methods for props
al2me6 Jun 15, 2025
4c74786
API to acquire the props instance containing stock properties
al2me6 Jun 15, 2025
fa301e8
keep the rim color intact in the part mpb
al2me6 Jun 15, 2025
dbf4bc2
the temp color is stored separately...
al2me6 Jun 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Source/DynamicProperties/Disposable.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
177 changes: 177 additions & 0 deletions Source/DynamicProperties/MaterialPropertyManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#nullable enable

using System.Collections.Generic;
using KSPBuildTools;
using UnityEngine;

namespace Shabby.DynamicProperties;

[KSPAddon(KSPAddon.Startup.EveryScene, false)]
public sealed class MaterialPropertyManager : MonoBehaviour
{
#region Fields

public static MaterialPropertyManager? Instance { get; private set; }

private readonly Dictionary<Renderer, PropsCascade> rendererCascades = [];

private readonly List<Props> propsLateUpdateQueue = [];

#endregion

#region Lifecycle

private MaterialPropertyManager()
{
}

private void Awake()
{
if (Instance != null) {
DestroyImmediate(this);
return;
}

name = nameof(MaterialPropertyManager);
Instance = this;
}

private void OnDestroy()
{
if (Instance != this) return;

Instance = null;
foreach (var cascade in rendererCascades.Values) cascade.Dispose();
MpbCompilerCache.CheckCleared();

// Poor man's GC :'(
PartPatch.CheckCleared();
MaterialColorUpdaterPatch.CheckCleared();
ModuleColorChangerPatch.CheckCleared();
FairingPanelPatch.CheckCleared();

this.LogMessage("destroyed");
}

#endregion

#region Public API

public bool Set(Renderer renderer, Props props)
{
if (!CheckRendererAlive(renderer)) return false;

if (!rendererCascades.TryGetValue(renderer, out var cascade)) {
rendererCascades[renderer] = cascade = new PropsCascade(renderer);
}

return cascade.Add(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 Unregister(Renderer renderer)
{
if (renderer.IsNullref()) return false;
if (!rendererCascades.Remove(renderer, out var cascade)) return false;
if (renderer.IsDestroyed()) this.LogDebug($"destroyed renderer {renderer.GetHashCode()}");
cascade.Dispose();
return true;
}

/// Get a reference to the `Props` instance containing the stock properties of the given
/// `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);
}

#endregion

private bool CheckRendererAlive(Renderer renderer)
{
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<Renderer> _destroyedRenderers = [];

internal void CheckRemoveDestroyedRenderers()
{
foreach (var renderer in rendererCascades.Keys) {
if (renderer.IsDestroyed()) _destroyedRenderers.Add(renderer);
}

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.IsDestroyed()) cascade.Remove(props);
}

CheckRemoveDestroyedRenderers();
}

private bool _propsUpdateScheduled = false;
private static readonly WaitForEndOfFrame WfEoF = new();

private IEnumerator<YieldInstruction> Co_PropsLateUpdate()
{
yield return WfEoF;

foreach (var props in propsLateUpdateQueue) {
if (props.NeedsEntriesUpdate) {
props.OnEntriesChanged?.Invoke(props);
} else if (props.NeedsValueUpdate) {
props.OnValueChanged?.Invoke(props, null);
}

props.SuppressEagerUpdate =
props.NeedsEntriesUpdate = props.NeedsValueUpdate = false;
}

propsLateUpdateQueue.Clear();
_propsUpdateScheduled = false;
}

internal void ScheduleLateUpdate(Props props)
{
propsLateUpdateQueue.Add(props);
if (_propsUpdateScheduled) return;
StartCoroutine(Co_PropsLateUpdate());
_propsUpdateScheduled = true;
}
}
140 changes: 140 additions & 0 deletions Source/DynamicProperties/MpbCompiler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#nullable enable

using System.Collections.Generic;
using System.Runtime.CompilerServices;
using KSPBuildTools;
using UnityEngine;

namespace Shabby.DynamicProperties;

internal class MpbCompiler : Disposable
{
#region Fields

/// Immutable.
internal readonly SortedSet<Props> Cascade;

private readonly HashSet<Renderer> managedRenderers = [];
private readonly MaterialPropertyBlock mpb = new();
private readonly Dictionary<int, Props> idManagers = [];

private static readonly MaterialPropertyBlock EmptyMpb = new();

#endregion

internal MpbCompiler(SortedSet<Props> cascade)
{
MaterialPropertyManager.Instance?.LogDebug(
$"new MpbCompiler instance {RuntimeHelpers.GetHashCode(this)}");

Cascade = cascade;
RebuildManagerMap();
RewriteMpb();
foreach (var props in Cascade) {
props.OnValueChanged += OnPropsValueChanged;
props.OnEntriesChanged += OnPropsEntriesChanged;
}
}

#region Renderer registration

internal void Register(Renderer renderer)
{
managedRenderers.Add(renderer);
Apply(renderer);
}

internal void Unregister(Renderer renderer)
{
managedRenderers.Remove(renderer);
if (!renderer.IsDestroyed()) renderer.SetPropertyBlock(EmptyMpb);

if (managedRenderers.Count > 0) return;
Log.Debug(
$"last renderer unregistered from MpbCompiler instance {RuntimeHelpers.GetHashCode(this)}");
MpbCompilerCache.Remove(this);
}

#endregion

#region Props updates

private void RebuildManagerMap()
{
idManagers.Clear();
foreach (var props in Cascade) {
foreach (var id in props.ManagedIds) {
idManagers[id] = props;
}
}
}

private void OnPropsEntriesChanged(Props props)
{
RebuildManagerMap();
RewriteMpb();
ApplyAll();
}

private void OnPropsValueChanged(Props props, int? id)
{
WriteMpb(props, id);
ApplyAll();
}

#endregion

#region Apply

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteMpb(Props props, int? id)
{
if (id.HasValue) {
var changedId = id.GetValueOrDefault();
if (idManagers[changedId] != props) return;
props.Write(changedId, mpb);
} else {
foreach (var (managedId, managingProps) in idManagers) {
if (props != managingProps) continue;
props.Write(managedId, mpb);
}
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RewriteMpb()
{
mpb.Clear();
foreach (var (id, props) in idManagers) props.Write(id, mpb);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Apply(Renderer renderer) => renderer.SetPropertyBlock(mpb);

private void ApplyAll()
{
var hasDestroyedRenderer = false;

foreach (var renderer in managedRenderers) {
if (renderer.IsDestroyed()) {
hasDestroyedRenderer = true;
} else {
Apply(renderer);
}
}

if (hasDestroyedRenderer) MaterialPropertyManager.Instance?.CheckRemoveDestroyedRenderers();
}

#endregion

protected override bool IsUnused() => managedRenderers.Count == 0;

protected override void OnDispose()
{
foreach (var props in Cascade) {
props.OnEntriesChanged -= OnPropsEntriesChanged;
props.OnValueChanged -= OnPropsValueChanged;
}
}
}
Loading