-
-
Save Narazaka/e125e3dd22e56fe9613378fdb58373c0 to your computer and use it in GitHub Desktop.
A window to see actual performance rank on building avatars.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Actual Performance Info Window | |
* https://gist.github.com/anatawa12/a4bb4e2e5d75b4fa5ba42e236aae564d | |
* | |
* Copy this cs file to anywhere in your asset folder is the only step to install this tool. | |
* | |
* A window to see actual performance rank on building avatars. | |
* When you click the `Build & Publish` button, this class will compute actual performance rank show you that. | |
* | |
* MIT License | |
* | |
* Copyright (c) 2023 anatawa12 | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
#if UNITY_EDITOR && (!ANATAWA12_GISTS_VPM_PACKAGE || GIST_a4bb4e2e5d75b4fa5ba42e236aae564d) | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using JetBrains.Annotations; | |
using UnityEditor; | |
using UnityEngine; | |
using UnityEngine.SceneManagement; | |
using VRC.Core; | |
using VRC.SDKBase; | |
using VRC.SDKBase.Editor.BuildPipeline; | |
using VRC.SDKBase.Validation.Performance; | |
using VRC.SDKBase.Validation.Performance.Stats; | |
namespace anatawa12.gists | |
{ | |
using static AvatarPerformanceCategory; | |
internal class ActualPerformanceWindow : EditorWindow, ISerializationCallbackReceiver | |
{ | |
[SerializeField] private AvatarPerformanceInfoSet[] avatars = Array.Empty<AvatarPerformanceInfoSet>(); | |
[SerializeField] private int selectingIndex; | |
[SerializeField] private Vector2 scroll; | |
[SerializeField] private bool calculatePc = true; | |
[SerializeField] private bool calculateAndroid = true; | |
// true if build is in progress. this is used to avoid build info update on play | |
[SerializeField] private bool isBuilding; | |
private bool IsBuilding => isBuilding; | |
private GUIContent[] _labels; | |
private void OnGUI() | |
{ | |
EditorGUILayout.BeginHorizontal(); | |
GUILayout.Label("Performance Rank for Previous build of ", Styles.TextStyle, GUILayout.MinHeight(32)); | |
EditorGUILayout.BeginVertical(); | |
EditorGUILayout.Space(); | |
selectingIndex = EditorGUILayout.Popup(selectingIndex, _labels); | |
EditorGUILayout.Space(); | |
EditorGUILayout.EndVertical(); | |
EditorGUILayout.BeginVertical(); | |
EditorGUILayout.Space(); | |
if (GUILayout.Button("Clear Avatars")) | |
{ | |
avatars = Array.Empty<AvatarPerformanceInfoSet>(); | |
_labels = Array.Empty<GUIContent>(); | |
} | |
EditorGUILayout.Space(); | |
EditorGUILayout.EndVertical(); | |
EditorGUILayout.EndHorizontal(); | |
EditorGUILayout.BeginHorizontal(); | |
calculatePc = EditorGUILayout.ToggleLeft("Calculate PC", calculatePc); | |
calculateAndroid = EditorGUILayout.ToggleLeft("Calculate Android", calculateAndroid); | |
EditorGUILayout.EndHorizontal(); | |
scroll = EditorGUILayout.BeginScrollView(scroll); | |
if (selectingIndex >= 0 && selectingIndex < avatars.Length) | |
{ | |
var avatar = avatars[selectingIndex]; | |
EditorGUILayout.BeginHorizontal(); | |
if (avatar.pc != null) | |
{ | |
EditorGUILayout.BeginVertical(); | |
EditorGUILayout.LabelField("PC", EditorStyles.boldLabel); | |
DisplayAvatarInfo(avatar.pc); | |
EditorGUILayout.EndVertical(); | |
} | |
if (avatar.android != null) | |
{ | |
EditorGUILayout.BeginVertical(); | |
EditorGUILayout.LabelField("Android", EditorStyles.boldLabel); | |
DisplayAvatarInfo(avatar.android); | |
EditorGUILayout.EndVertical(); | |
} | |
EditorGUILayout.EndHorizontal(); | |
} | |
else | |
GUILayout.Label("No avatars selecting", GUILayout.Height(32)); | |
EditorGUILayout.EndScrollView(); | |
} | |
private void DisplayAvatarInfo(AvatarPerformanceInfo avatar) | |
{ | |
DisplayRating(avatar.overall.rating, $"Overall Rating: {avatar.overall.rating}"); | |
foreach (var performanceInfo in avatar.info) | |
DisplayRating(performanceInfo.rating, | |
$"{performanceInfo.categoryName}: {performanceInfo.rating} ({performanceInfo.data})"); | |
} | |
private static void DisplayRating(PerformanceRating rating, string message) | |
{ | |
EditorGUILayout.BeginHorizontal(); | |
GUILayout.Label(new GUIContent(PerformanceIcons.GetIconForPerformance(rating)), GUILayout.Height(32), | |
GUILayout.Width(32)); | |
GUILayout.Label(message, Styles.TextStyle, GUILayout.MinHeight(32)); | |
GUILayout.FlexibleSpace(); | |
EditorGUILayout.EndHorizontal(); | |
} | |
[NotNull] | |
private static ActualPerformanceWindow GetWindowInstance() => GetWindow<ActualPerformanceWindow>("Actual Performance"); | |
[CanBeNull] | |
private static ActualPerformanceWindow TryGetWindowInstance() | |
{ | |
var objects = Resources.FindObjectsOfTypeAll<ActualPerformanceWindow>(); | |
return objects.Length == 0 ? null : objects[0]; | |
} | |
private void AddInfo(in AvatarPerformanceInfoSet performanceInfo) | |
{ | |
EditorUtility.SetDirty(this); | |
if (avatars == null) avatars = Array.Empty<AvatarPerformanceInfoSet>(); | |
for (var i = 0; i < avatars.Length; i++) | |
{ | |
if (avatars[i].avatarName == performanceInfo.avatarName) | |
{ | |
avatars[i] = performanceInfo; | |
selectingIndex = i; | |
return; | |
} | |
} | |
// not found: add | |
ArrayUtility.Add(ref avatars, performanceInfo); | |
selectingIndex = avatars.Length - 1; | |
ResetLabels(); | |
} | |
private void MarkBuilding() | |
{ | |
isBuilding = true; | |
EditorUtility.SetDirty(this); | |
} | |
private void ClearBuilding() | |
{ | |
isBuilding = false; | |
EditorUtility.SetDirty(this); | |
} | |
private void ResetLabels() | |
{ | |
// \u2215: ∕ division slash, which is similar to slash | |
_labels = avatars.Select(x => new GUIContent(x.avatarName)).ToArray(); | |
} | |
void ISerializationCallbackReceiver.OnBeforeSerialize() | |
{ | |
} | |
void ISerializationCallbackReceiver.OnAfterDeserialize() | |
{ | |
ResetLabels(); | |
} | |
static class Styles | |
{ | |
public static GUIStyle TextStyle = new GUIStyle(EditorStyles.label) { wordWrap = true }; | |
} | |
static class PerformanceIcons | |
{ | |
private static Texture _excellent; | |
private static Texture _good; | |
private static Texture _medium; | |
private static Texture _poor; | |
private static Texture _veryPoor; | |
public static Texture Excellent => _excellent ? _excellent : _excellent = LoadIcon("Great"); | |
public static Texture Good => _good ? _good : _good = LoadIcon("Good"); | |
public static Texture Medium => _medium ? _medium : _medium = LoadIcon("Medium"); | |
public static Texture Poor => _poor ? _poor : _poor = LoadIcon("Poor"); | |
public static Texture VeryPoor => _veryPoor ? _veryPoor : _veryPoor = LoadIcon("Horrible"); | |
public static Texture GetIconForPerformance(PerformanceRating rating) | |
{ | |
switch (rating) | |
{ | |
case PerformanceRating.Excellent: | |
return Excellent; | |
case PerformanceRating.Good: | |
return Good; | |
case PerformanceRating.Medium: | |
return Medium; | |
case PerformanceRating.Poor: | |
return Poor; | |
case PerformanceRating.VeryPoor: | |
return VeryPoor; | |
case PerformanceRating.None: | |
return null; | |
default: | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
private static Texture LoadIcon(string texName) | |
{ | |
return Resources.Load<Texture>($"PerformanceIcons/Perf_{texName}_32"); | |
} | |
} | |
[Serializable] | |
private class AvatarPerformanceInfoSet | |
{ | |
[SerializeField] public string avatarName; | |
[SerializeField] public AvatarPerformanceInfo pc; | |
[SerializeField] public AvatarPerformanceInfo android; | |
public AvatarPerformanceInfo current => PerformanceRankComputer.CurrentIsMobile ? android : pc; | |
public AvatarPerformanceInfoSet(string avatarName, AvatarPerformanceInfo pc = null, AvatarPerformanceInfo android = null) | |
{ | |
this.avatarName = avatarName; | |
this.pc = pc; | |
this.android = android; | |
} | |
} | |
[Serializable] | |
private class AvatarPerformanceInfo | |
{ | |
[SerializeField] public PerformanceInfo[] info; | |
[SerializeField] public PerformanceInfo overall; | |
public AvatarPerformanceInfo(PerformanceInfo[] info, PerformanceInfo overall) | |
{ | |
Array.Sort(info, (x, y) => -x.rating.CompareTo(y.rating)); | |
this.info = info; | |
this.overall = overall; | |
} | |
} | |
[Serializable] | |
private struct PerformanceInfo | |
{ | |
public AvatarPerformanceCategory category; | |
public string categoryName; | |
public string data; | |
public PerformanceRating rating; | |
} | |
private class ActualPerformanceCallback : IVRCSDKPreprocessAvatarCallback | |
{ | |
// run at last | |
public int callbackOrder => int.MaxValue; | |
public bool OnPreprocessAvatar(GameObject avatarGameObject) | |
{ | |
var window = GetWindowInstance(); | |
window.MarkBuilding(); | |
var name = avatarGameObject.name; | |
// strip (Clone) at end | |
if (name.EndsWith("(Clone)", StringComparison.Ordinal)) | |
name = name.Substring(0, name.Length - "(Clone)".Length); | |
var info = new AvatarPerformanceInfoSet( | |
name, | |
pc: window.calculatePc ? PerformanceRankComputer.Compute(avatarGameObject, false) : null, | |
android: window.calculateAndroid ? PerformanceRankComputer.Compute(avatarGameObject, true) : null | |
); | |
window.AddInfo(info); | |
if (info.current != null) UpdateFallbackStatus(avatarGameObject, info.current); | |
return true; | |
} | |
private void UpdateFallbackStatus(GameObject avatarGameObject, AvatarPerformanceInfo info) | |
{ | |
var pipeline = avatarGameObject.GetComponent<PipelineManager>(); | |
if (pipeline == null) return; | |
if (pipeline.fallbackStatus != PipelineManager.FallbackStatus.InvalidPerformance) return; | |
if (info.overall.rating <= PerformanceRating.Good) | |
pipeline.fallbackStatus = PipelineManager.FallbackStatus.Valid; | |
} | |
} | |
private static class PerformanceRankComputer | |
{ | |
public static AvatarPerformanceInfo Compute(GameObject avatarGameObject, bool isMobile) | |
{ | |
var stats = new AvatarPerformanceStats(isMobile); | |
AvatarPerformance.CalculatePerformanceStats(avatarGameObject.name, avatarGameObject, stats, isMobile); | |
var info = new List<PerformanceInfo>(); | |
foreach (var category in Enum.GetValues(typeof(AvatarPerformanceCategory)) | |
.Cast<AvatarPerformanceCategory>()) | |
{ | |
var categoryName = TryGetCategoryName(category); | |
if (categoryName == null) continue; | |
info.Add(new PerformanceInfo | |
{ | |
category = category, | |
categoryName = categoryName, | |
data = PerformanceData(stats, category), | |
rating = stats.GetPerformanceRatingForCategory(category), | |
}); | |
} | |
var overall = new PerformanceInfo | |
{ | |
category = Overall, | |
categoryName = "Overall", | |
data = "", | |
rating = stats.GetPerformanceRatingForCategory(Overall), | |
}; | |
return new AvatarPerformanceInfo(info.ToArray(), overall); | |
} | |
public static bool CurrentIsMobile | |
{ | |
get => EditorUserBuildSettings.selectedBuildTargetGroup != BuildTargetGroup.Standalone; | |
} | |
private static string PerformanceData(AvatarPerformanceStats stats, AvatarPerformanceCategory category) | |
{ | |
switch (category) | |
{ | |
case None: return "(none)"; | |
case Overall: return "(none)"; | |
case DownloadSize: return $"{stats.downloadSize}"; | |
case PolyCount: return $"{stats.polyCount}"; | |
case AABB: return $"{stats.aabb}"; | |
case SkinnedMeshCount: return $"{stats.skinnedMeshCount}"; | |
case MeshCount: return $"{stats.meshCount}"; | |
case MaterialCount: return $"{stats.materialCount}"; | |
case DynamicBoneComponentCount: return $"{stats.dynamicBone?.componentCount}"; | |
case DynamicBoneSimulatedBoneCount: return $"{stats.dynamicBone?.transformCount}"; | |
case DynamicBoneColliderCount: return $"{stats.dynamicBone?.colliderCount}"; | |
case DynamicBoneCollisionCheckCount: return $"{stats.dynamicBone?.collisionCheckCount}"; | |
case PhysBoneComponentCount: return $"{stats.physBone?.componentCount}"; | |
case PhysBoneTransformCount: return $"{stats.physBone?.transformCount}"; | |
case PhysBoneColliderCount: return $"{stats.physBone?.colliderCount}"; | |
case PhysBoneCollisionCheckCount: return $"{stats.physBone?.collisionCheckCount}"; | |
case ContactCount: return $"{stats.contactCount}"; | |
case AnimatorCount: return $"{stats.animatorCount}"; | |
case BoneCount: return $"{stats.boneCount}"; | |
case LightCount: return $"{stats.lightCount}"; | |
case ParticleSystemCount: return $"{stats.particleSystemCount}"; | |
case ParticleTotalCount: return $"{stats.particleTotalCount}"; | |
case ParticleMaxMeshPolyCount: return $"{stats.particleMaxMeshPolyCount}"; | |
case ParticleTrailsEnabled: return $"{stats.particleTrailsEnabled}"; | |
case ParticleCollisionEnabled: return $"{stats.particleCollisionEnabled}"; | |
case TrailRendererCount: return $"{stats.trailRendererCount}"; | |
case LineRendererCount: return $"{stats.lineRendererCount}"; | |
case ClothCount: return $"{stats.clothCount}"; | |
case ClothMaxVertices: return $"{stats.clothMaxVertices}"; | |
case PhysicsColliderCount: return $"{stats.physicsColliderCount}"; | |
case PhysicsRigidbodyCount: return $"{stats.physicsRigidbodyCount}"; | |
case AudioSourceCount: return $"{stats.audioSourceCount}"; | |
case TextureMegabytes: return $"{stats.textureMegabytes}"; | |
case AvatarPerformanceCategoryCount: return "(none)"; | |
default: return "(unknown)"; | |
} | |
} | |
private static string TryGetCategoryName(AvatarPerformanceCategory category) | |
{ | |
try | |
{ | |
return AvatarPerformanceStats.GetPerformanceCategoryDisplayName(category); | |
} | |
catch | |
{ | |
// GetPerformanceCategoryDisplayName may throw KeyNotFoundException | |
return null; | |
} | |
} | |
} | |
[InitializeOnLoad] | |
private static class ComputeOnPlay | |
{ | |
private const string EnableMenuName = "Tools/anatawa12 gists/Compute actual Performance on Play"; | |
private const string EnableSettingName = "com.anatawa12.gist.compute-actual-performance-on-play"; | |
private static bool _computeDone; | |
public static bool Enabled | |
{ | |
get => EditorPrefs.GetBool(EnableSettingName, true); | |
set => EditorPrefs.SetBool(EnableSettingName, value); | |
} | |
static ComputeOnPlay() | |
{ | |
EditorApplication.delayCall += () => Menu.SetChecked(EnableMenuName, Enabled); | |
EditorApplication.update += OnUpdate; | |
EditorApplication.playModeStateChanged += PlaymodeChanged; | |
} | |
private static void OnUpdate() | |
{ | |
if (EditorApplication.isPlaying) | |
{ | |
if (_computeDone) return; // already computed | |
_computeDone = true; | |
if (!Enabled) return; | |
var window = GetWindowInstance(); | |
if (window.IsBuilding) | |
{ | |
Debug.Log("Skipping the computing because this play is avatar info input play"); | |
return; // build is in progress | |
} | |
foreach (var vrcAvatarDescriptor in Enumerable.Range(0, SceneManager.sceneCount) | |
.Select(SceneManager.GetSceneAt) | |
.SelectMany(x => x.GetRootGameObjects()) | |
.SelectMany(x => x.GetComponentsInChildren<VRC_AvatarDescriptor>())) | |
{ | |
var gameObject = vrcAvatarDescriptor.gameObject; | |
window.AddInfo( | |
new AvatarPerformanceInfoSet(gameObject.name + " (Play)", | |
pc: window.calculatePc ? PerformanceRankComputer.Compute(gameObject, false) : null, | |
android: window.calculateAndroid ? PerformanceRankComputer.Compute(gameObject, true) : null | |
)); | |
} | |
} | |
else | |
{ | |
_computeDone = false; | |
} | |
} | |
private static void PlaymodeChanged(PlayModeStateChange obj) | |
{ | |
if (obj != PlayModeStateChange.ExitingPlayMode) return; | |
TryGetWindowInstance()?.ClearBuilding(); | |
} | |
[MenuItem(EnableMenuName)] | |
private static void ToggleApplyOnPlay() | |
{ | |
Enabled = !Enabled; | |
Menu.SetChecked(EnableMenuName, Enabled); | |
} | |
} | |
} | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment