From 9530ed8dc03b604ca6cf7a05dd811d7ae073b297 Mon Sep 17 00:00:00 2001 From: bd_ Date: Sun, 3 Nov 2024 20:04:26 -0800 Subject: [PATCH] perf: optimize RemoveUnusedMaterialProperties This change improves the performance of RemoveUnusedMaterialProperties, by using native SerializedProperty calls to move items, rather than a C#-side copy function. This avoids a lot of marshalling and GC. This provides a performance improvement of 340ms -> 68ms on my main avatar. --- .../RemoveUnusedMaterialProperties.cs | 134 +++++++++++++++--- 1 file changed, 118 insertions(+), 16 deletions(-) diff --git a/Editor/Processors/TraceAndOptimize/RemoveUnusedMaterialProperties.cs b/Editor/Processors/TraceAndOptimize/RemoveUnusedMaterialProperties.cs index d4ee1146..5f10612e 100644 --- a/Editor/Processors/TraceAndOptimize/RemoveUnusedMaterialProperties.cs +++ b/Editor/Processors/TraceAndOptimize/RemoveUnusedMaterialProperties.cs @@ -1,8 +1,10 @@ +using System; using System.Collections.Generic; using System.Linq; using nadena.dev.ndmf; using UnityEditor; using UnityEngine; +using UnityEngine.Profiling; namespace Anatawa12.AvatarOptimizer.Processors.TraceAndOptimizes { @@ -49,6 +51,47 @@ void CleanMaterial(IEnumerable materials) } } + [Serializable] + class StringContainer : ScriptableSingleton + { + public string? theString; + } + + class ShaderInfo + { + private HashSet properties; + + public bool HasProperty(SerializedProperty prop) + { + return properties.Contains(prop.contentHash); + } + + public ShaderInfo(Shader shader) + { + Profiler.BeginSample("ShaderInfo.ctor", shader); + var props = shader.GetPropertyCount(); + var serializedProp = new SerializedObject(StringContainer.instance) + .FindProperty(nameof(StringContainer.theString)); + + properties = new HashSet(props + fallbackShaderProperties.Count); + + for (int i = 0; i < props; i++) + { + serializedProp.stringValue = shader.GetPropertyName(i); + properties.Add(serializedProp.contentHash); + } + + foreach (var fallbackPropName in fallbackShaderProperties) + { + serializedProp.stringValue = fallbackPropName; + properties.Add(serializedProp.contentHash); + } + Profiler.EndSample(); + } + } + + private Dictionary _shaderInfoCache = new(); + // Algorithm is based on lilToon or thr following blog post, // but speed up with not using DeleteArrayElementAtIndex. // @@ -57,40 +100,99 @@ void CleanMaterial(IEnumerable materials) // https://github.com/lilxyzw/lilToon/blob/b96470d3dd9092b840052578048b2307fe6d8786/Assets/lilToon/Editor/lilMaterialUtils.cs#L658-L686 // // https://light11.hatenadiary.com/entry/2018/12/04/224253 - private static void RemoveUnusedProperties(Material material) + private void RemoveUnusedProperties(Material material) { + var shader = material.shader; + if (shader == null) return; + + if (!_shaderInfoCache.TryGetValue(shader, out var shaderInfo)) + { + shaderInfo = new ShaderInfo(shader); + _shaderInfoCache.Add(shader, shaderInfo); + } + using var so = new SerializedObject(material); - DeleteUnusedProperties(so.FindProperty("m_SavedProperties.m_TexEnvs"), material); - DeleteUnusedProperties(so.FindProperty("m_SavedProperties.m_Floats"), material); - DeleteUnusedProperties(so.FindProperty("m_SavedProperties.m_Colors"), material); + DeleteUnusedProperties(so.FindProperty("m_SavedProperties.m_TexEnvs"), material, shaderInfo); + DeleteUnusedProperties(so.FindProperty("m_SavedProperties.m_Floats"), material, shaderInfo); + DeleteUnusedProperties(so.FindProperty("m_SavedProperties.m_Colors"), material, shaderInfo); so.ApplyModifiedProperties(); } - private static void DeleteUnusedProperties(SerializedProperty props, Material material) + private void DeleteUnusedProperties(SerializedProperty props, Material material, ShaderInfo shaderInfo) { if (props.arraySize == 0) return; + + // Using Utils.CopyDataFrom generates a lot of garbage. Instead, we'll let the native side move stuff + // around for us; we'll generate a plan for how to move things, then execute it at the end. + + List<(int, int)> fromTo = new List<(int, int)>(props.arraySize); - var destCount = 0; + var arrayLen = props.arraySize; + var srcIndex = 0; + var destIndex = 0; for ( - SerializedProperty srcIter = props.GetArrayElementAtIndex(0), - destIter = srcIter.Copy(), - srcEnd = props.GetEndProperty(); - !SerializedProperty.EqualContents(srcIter, srcEnd); + SerializedProperty srcIter = props.GetArrayElementAtIndex(0); + srcIndex < arrayLen; srcIter.NextVisible(false) ) { - var porpertyName = srcIter.FindPropertyRelative("first").stringValue; - if (material.HasProperty(porpertyName) || fallbackShaderProperties.Contains(porpertyName)) + var propertyName = srcIter.FindPropertyRelative("first"); + + if (shaderInfo.HasProperty(propertyName)) { - destIter.CopyDataFrom(srcIter); - destIter.NextVisible(false); - destCount++; + if (destIndex != srcIndex) + { + fromTo.Add((srcIndex, destIndex)); + } + + destIndex++; } + + srcIndex++; + } + + Profiler.BeginSample("Apply moves"); + int sourceOffset = 0; + int remainingMoves = fromTo.Count; + var remainingArraySize = props.arraySize; + foreach (var (originalFrom, to) in fromTo) + { + // Compute whether we want to delete any skipped array elements. + // If we delete, we have to move (once) every element after the element in question. + // However, if we don't, then every subsequent retained element will move this element. + + // If we do delete, we must adjust sourceOffset to ensure we're reading from the right place. + var from = originalFrom + sourceOffset; + while (from != to) + { + var costToRetain = remainingMoves; + var costToDelete = remainingArraySize - from - 1; + + if (costToDelete < costToRetain) + { + props.DeleteArrayElementAtIndex(from); + sourceOffset--; + remainingArraySize--; + from--; + } + else + { + break; + } + } + + if (from == to) continue; + + // This MoveArrayElement call effectively rotates the range of elements + // between from and to. As such, since we are iterating from the start, + // each prior rotation doesn't affect the "from" index of subsequent elements. + props.MoveArrayElement(from, to); } + Profiler.EndSample(); - props.arraySize = destCount; + props.arraySize = destIndex; } // TODO: change set of properties by fallback shader names