Skip to content

Commit

Permalink
perf: optimize RemoveUnusedMaterialProperties
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bdunderscore committed Nov 4, 2024
1 parent 9ea7c9a commit 9530ed8
Showing 1 changed file with 118 additions and 16 deletions.
134 changes: 118 additions & 16 deletions Editor/Processors/TraceAndOptimize/RemoveUnusedMaterialProperties.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -49,6 +51,47 @@ void CleanMaterial(IEnumerable<Material?> materials)
}
}

[Serializable]
class StringContainer : ScriptableSingleton<StringContainer>
{
public string? theString;
}

class ShaderInfo
{
private HashSet<uint> 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<uint>(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<Shader, ShaderInfo> _shaderInfoCache = new();

// Algorithm is based on lilToon or thr following blog post,
// but speed up with not using DeleteArrayElementAtIndex.
//
Expand All @@ -57,40 +100,99 @@ void CleanMaterial(IEnumerable<Material?> 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
Expand Down

0 comments on commit 9530ed8

Please sign in to comment.