using System; using System.Collections.Generic; using NBShader; using UnityEditor; using UnityEngine; namespace NBShaders2.Editor.FeatureLevel { [FilePath("ProjectSettings/NBShaderFeatureLevels.asset", FilePathAttribute.Location.ProjectFolder)] public sealed class NBShaderFeatureLevelProjectSettings : ScriptableSingleton { private static readonly string[] DefaultQualityNames = { "Default" }; [SerializeField] private NBShaderFeatureTierKeywordSet[] m_TierKeywordSets; [SerializeField] private NBShaderFeatureTierPassSet[] m_TierPassSets; [SerializeField] private NBShaderQualityTierMapping[] m_QualityTierMappings; [SerializeField] private bool m_EnableDebugSymbols; [NonSerialized] private bool m_Initialized; [NonSerialized] private HashSet[] m_AllowedKeywordSetCache; [NonSerialized] private bool m_AllowedKeywordSetCacheValid; [NonSerialized] private HashSet[] m_AllowedPassFeatureSetCache; [NonSerialized] private bool m_AllowedPassFeatureSetCacheValid; public NBShaderFeatureTierKeywordSet[] tierKeywordSets { get { EnsureInitialized(); InvalidateAllowedKeywordSetCache(); return m_TierKeywordSets; } } public NBShaderFeatureTierPassSet[] tierPassSets { get { EnsureInitialized(); InvalidateAllowedPassFeatureSetCache(); return m_TierPassSets; } } public NBShaderQualityTierMapping[] qualityTierMappings { get { EnsureInitialized(); return m_QualityTierMappings; } } public bool enableDebugSymbols { get { return m_EnableDebugSymbols; } } public void EnsureInitialized() { if (m_Initialized && HasValidTierKeywordSets() && HasValidTierPassSets() && HasValidQualityMappings() && AreQualityMappingsCurrent()) { return; } var changed = false; if (m_TierKeywordSets == null || m_TierKeywordSets.Length != 4) { ResetTierKeywordSetsToDefault(); changed = true; } else { changed |= NormalizeTierKeywordSets(); } if (m_TierPassSets == null || m_TierPassSets.Length != 4) { ResetTierPassSetsToDefault(); changed = true; } else { changed |= NormalizeTierPassSets(); } if (m_QualityTierMappings == null || m_QualityTierMappings.Length == 0) { ResetQualityMappingsToDefault(); changed = true; } else { changed |= NormalizeQualityMappingsWithCurrentQualityLevels(); } if (changed) { Save(true); } m_Initialized = true; } public void ResetTierKeywordSetsToDefault() { m_TierKeywordSets = NBShaderFeatureLevelPresetLoader.LoadDefaultTierKeywordSets(); InvalidateAllowedKeywordSetCache(); } public void ResetTierPassSetsToDefault() { m_TierPassSets = NBShaderFeatureLevelPresetLoader.LoadDefaultTierPassSets(); InvalidateAllowedPassFeatureSetCache(); } public void ResetQualityMappingsToDefault() { var names = QualitySettings.names; if (names == null || names.Length == 0) names = DefaultQualityNames; m_QualityTierMappings = new NBShaderQualityTierMapping[names.Length]; for (var i = 0; i < names.Length; i++) { m_QualityTierMappings[i] = new NBShaderQualityTierMapping { qualityName = names[i], tier = GuessTierForQualityIndex(i, names.Length) }; } } public HashSet GetAllowedKeywordSet(NBShaderFeatureTier tier) { return new HashSet(GetAllowedKeywordSetForReadOnlyUse(tier), StringComparer.Ordinal); } public HashSet GetAllowedPassFeatureSet(NBShaderFeatureTier tier) { return new HashSet(GetAllowedPassFeatureSetForReadOnlyUse(tier), StringComparer.Ordinal); } public bool IsKeywordAllowed(NBShaderFeatureTier tier, string keyword) { return GetAllowedKeywordSetForReadOnlyUse(tier).Contains(keyword); } public bool IsPassFeatureAllowed(NBShaderFeatureTier tier, string passFeatureId) { return GetAllowedPassFeatureSetForReadOnlyUse(tier).Contains(passFeatureId); } internal HashSet GetAllowedKeywordSetForReadOnlyUse(NBShaderFeatureTier tier) { EnsureInitialized(); EnsureAllowedKeywordSetCache(); return m_AllowedKeywordSetCache[ToTierIndex(tier)]; } internal HashSet GetAllowedPassFeatureSetForReadOnlyUse(NBShaderFeatureTier tier) { EnsureInitialized(); EnsureAllowedPassFeatureSetCache(); return m_AllowedPassFeatureSetCache[ToTierIndex(tier)]; } internal HashSet GetAllowedKeywordSetForBuildInfoNoSave(NBShaderFeatureTier tier) { var existing = FindTierKeywordSet(tier); if (existing != null && existing.allowedKeywords != null) return BuildSanitizedAllowedSet(existing.allowedKeywords); return BuildSanitizedAllowedSet(NBShaderFeatureLevelCatalog.ManagedKeywords); } internal HashSet GetAllowedPassFeatureSetForBuildInfoNoSave(NBShaderFeatureTier tier) { var existing = FindTierPassSet(tier); if (existing != null && existing.allowedPassFeatures != null) return BuildSanitizedAllowedPassFeatureSet(existing.allowedPassFeatures); return BuildSanitizedAllowedPassFeatureSet(NBShaderFeatureLevelCatalog.ManagedPassFeatures); } public void SetKeywordAllowed(NBShaderFeatureTier tier, string keyword, bool allowed) { EnsureInitialized(); if (!NBShaderFeatureLevelCatalog.IsManagedKeyword(keyword)) return; NBShaderFeatureTierKeywordSet target = null; for (var i = 0; i < m_TierKeywordSets.Length; i++) { if (m_TierKeywordSets[i] != null && m_TierKeywordSets[i].tier == tier) { target = m_TierKeywordSets[i]; break; } } if (target == null) { target = new NBShaderFeatureTierKeywordSet { tier = tier }; var index = (int)tier; if (index >= 0 && index < m_TierKeywordSets.Length) m_TierKeywordSets[index] = target; } var keywords = BuildSanitizedAllowedSet(target.allowedKeywords); if (allowed) keywords.Add(keyword); else keywords.Remove(keyword); target.allowedKeywords = ToCatalogOrderedArray(keywords); InvalidateAllowedKeywordSetCache(); } public void SetPassFeatureAllowed(NBShaderFeatureTier tier, string passFeatureId, bool allowed) { EnsureInitialized(); if (!NBShaderFeatureLevelCatalog.IsManagedPassFeature(passFeatureId)) return; NBShaderFeatureTierPassSet target = null; for (var i = 0; i < m_TierPassSets.Length; i++) { if (m_TierPassSets[i] != null && m_TierPassSets[i].tier == tier) { target = m_TierPassSets[i]; break; } } if (target == null) { target = new NBShaderFeatureTierPassSet { tier = tier }; var index = (int)tier; if (index >= 0 && index < m_TierPassSets.Length) m_TierPassSets[index] = target; } var passFeatures = BuildSanitizedAllowedPassFeatureSet(target.allowedPassFeatures); if (allowed) passFeatures.Add(passFeatureId); else passFeatures.Remove(passFeatureId); target.allowedPassFeatures = ToCatalogOrderedPassFeatureArray(passFeatures); InvalidateAllowedPassFeatureSetCache(); } public bool TryGetTierForQualityName(string qualityName, out NBShaderFeatureTier tier) { EnsureInitialized(); tier = NBShaderFeatureTier.Ultra; return TryGetTierForQualityNameNoSave(qualityName, out tier); } internal bool TryGetTierForQualityNameNoSave(string qualityName, out NBShaderFeatureTier tier) { tier = NBShaderFeatureTier.Ultra; if (string.IsNullOrEmpty(qualityName)) return false; if (m_QualityTierMappings != null) { for (var i = 0; i < m_QualityTierMappings.Length; i++) { var mapping = m_QualityTierMappings[i]; if (mapping == null || !string.Equals(mapping.qualityName, qualityName, StringComparison.Ordinal)) continue; tier = mapping.tier; return true; } } var names = QualitySettings.names; if (names == null || names.Length == 0) names = DefaultQualityNames; for (var i = 0; i < names.Length; i++) { if (!string.Equals(names[i], qualityName, StringComparison.Ordinal)) continue; tier = GuessTierForQualityIndex(i, names.Length); return true; } return false; } public string[] GetQualityNamesForTier(NBShaderFeatureTier tier) { EnsureInitialized(); if (m_QualityTierMappings == null || m_QualityTierMappings.Length == 0) return new string[0]; var names = new List(); for (var i = 0; i < m_QualityTierMappings.Length; i++) { var mapping = m_QualityTierMappings[i]; if (mapping != null && mapping.tier == tier && !string.IsNullOrEmpty(mapping.qualityName)) names.Add(mapping.qualityName); } return names.ToArray(); } public void MoveQualityToTier(string qualityName, NBShaderFeatureTier tier) { EnsureInitialized(); if (string.IsNullOrEmpty(qualityName)) return; for (var i = 0; i < m_QualityTierMappings.Length; i++) { var mapping = m_QualityTierMappings[i]; if (mapping == null || !string.Equals(mapping.qualityName, qualityName, StringComparison.Ordinal)) continue; mapping.tier = tier; return; } var newMappings = new NBShaderQualityTierMapping[m_QualityTierMappings.Length + 1]; Array.Copy(m_QualityTierMappings, newMappings, m_QualityTierMappings.Length); newMappings[newMappings.Length - 1] = new NBShaderQualityTierMapping { qualityName = qualityName, tier = tier }; m_QualityTierMappings = newMappings; } public void SaveProjectSettings() { EnsureInitialized(); Save(true); } public void SetDebugSymbolsEnabled(bool enabled) { m_EnableDebugSymbols = enabled; } public void SaveDebugSymbolsProjectSettings() { Save(true); } private static NBShaderFeatureTier GuessTierForQualityIndex(int index, int count) { if (count <= 1) return NBShaderFeatureTier.Ultra; var normalized = index / (float)(count - 1); if (normalized < 0.25f) return NBShaderFeatureTier.Low; if (normalized < 0.50f) return NBShaderFeatureTier.Medium; if (normalized < 0.75f) return NBShaderFeatureTier.High; return NBShaderFeatureTier.Ultra; } private bool NormalizeTierKeywordSets() { var changed = false; NBShaderFeatureTierKeywordSet[] defaults = null; var normalized = new NBShaderFeatureTierKeywordSet[4]; for (var tierIndex = 0; tierIndex < normalized.Length; tierIndex++) { var tier = (NBShaderFeatureTier)tierIndex; var existing = FindTierKeywordSet(tier); if (existing == null) { if (defaults == null) defaults = NBShaderFeatureLevelPresetLoader.LoadDefaultTierKeywordSets(); changed = true; } normalized[tierIndex] = new NBShaderFeatureTierKeywordSet { tier = tier, allowedKeywords = existing != null ? ToCatalogOrderedArray(BuildSanitizedAllowedSet(existing.allowedKeywords)) : GetAllowedKeywords(defaults, tier) }; if (existing != null && !AreKeywordArraysEquivalent(existing.allowedKeywords, normalized[tierIndex].allowedKeywords)) changed = true; } m_TierKeywordSets = normalized; if (changed) InvalidateAllowedKeywordSetCache(); return changed; } private bool NormalizeTierPassSets() { var changed = false; NBShaderFeatureTierPassSet[] defaults = null; var normalized = new NBShaderFeatureTierPassSet[4]; for (var tierIndex = 0; tierIndex < normalized.Length; tierIndex++) { var tier = (NBShaderFeatureTier)tierIndex; var existing = FindTierPassSet(tier); if (existing == null) { if (defaults == null) defaults = NBShaderFeatureLevelPresetLoader.LoadDefaultTierPassSets(); changed = true; } normalized[tierIndex] = new NBShaderFeatureTierPassSet { tier = tier, allowedPassFeatures = existing != null ? ToCatalogOrderedPassFeatureArray(BuildSanitizedAllowedPassFeatureSet(existing.allowedPassFeatures)) : GetAllowedPassFeatures(defaults, tier) }; if (existing != null && !ArePassFeatureArraysEquivalent(existing.allowedPassFeatures, normalized[tierIndex].allowedPassFeatures)) changed = true; } m_TierPassSets = normalized; if (changed) InvalidateAllowedPassFeatureSetCache(); return changed; } private bool HasValidTierKeywordSets() { if (m_TierKeywordSets == null || m_TierKeywordSets.Length != 4) return false; for (var i = 0; i < m_TierKeywordSets.Length; i++) { var set = m_TierKeywordSets[i]; if (set == null || set.tier != (NBShaderFeatureTier)i || set.allowedKeywords == null) return false; } return true; } private bool HasValidTierPassSets() { if (m_TierPassSets == null || m_TierPassSets.Length != 4) return false; for (var i = 0; i < m_TierPassSets.Length; i++) { var set = m_TierPassSets[i]; if (set == null || set.tier != (NBShaderFeatureTier)i || set.allowedPassFeatures == null) return false; } return true; } private bool HasValidQualityMappings() { return m_QualityTierMappings != null && m_QualityTierMappings.Length > 0; } private bool AreQualityMappingsCurrent() { var names = QualitySettings.names; if (names == null || names.Length == 0) names = DefaultQualityNames; if (m_QualityTierMappings == null || m_QualityTierMappings.Length != names.Length) return false; for (var i = 0; i < names.Length; i++) { var mapping = m_QualityTierMappings[i]; if (mapping == null || !string.Equals(mapping.qualityName, names[i], StringComparison.Ordinal)) return false; } return true; } private void EnsureAllowedKeywordSetCache() { if (m_AllowedKeywordSetCacheValid && m_AllowedKeywordSetCache != null && m_AllowedKeywordSetCache.Length == 4) { return; } if (m_AllowedKeywordSetCache == null || m_AllowedKeywordSetCache.Length != 4) m_AllowedKeywordSetCache = new HashSet[4]; for (var i = 0; i < m_AllowedKeywordSetCache.Length; i++) m_AllowedKeywordSetCache[i] = BuildAllowedKeywordSet((NBShaderFeatureTier)i); m_AllowedKeywordSetCacheValid = true; } private void EnsureAllowedPassFeatureSetCache() { if (m_AllowedPassFeatureSetCacheValid && m_AllowedPassFeatureSetCache != null && m_AllowedPassFeatureSetCache.Length == 4) { return; } if (m_AllowedPassFeatureSetCache == null || m_AllowedPassFeatureSetCache.Length != 4) m_AllowedPassFeatureSetCache = new HashSet[4]; for (var i = 0; i < m_AllowedPassFeatureSetCache.Length; i++) m_AllowedPassFeatureSetCache[i] = BuildAllowedPassFeatureSet((NBShaderFeatureTier)i); m_AllowedPassFeatureSetCacheValid = true; } private void InvalidateAllowedKeywordSetCache() { m_AllowedKeywordSetCacheValid = false; } private void InvalidateAllowedPassFeatureSetCache() { m_AllowedPassFeatureSetCacheValid = false; } private HashSet BuildAllowedKeywordSet(NBShaderFeatureTier tier) { var result = new HashSet(StringComparer.Ordinal); if (m_TierKeywordSets == null) return result; for (var i = 0; i < m_TierKeywordSets.Length; i++) { var set = m_TierKeywordSets[i]; if (set == null || set.tier != tier || set.allowedKeywords == null) continue; for (var k = 0; k < set.allowedKeywords.Length; k++) { var keyword = set.allowedKeywords[k]; if (NBShaderFeatureLevelCatalog.IsManagedKeyword(keyword)) result.Add(keyword); } } return result; } private HashSet BuildAllowedPassFeatureSet(NBShaderFeatureTier tier) { var result = new HashSet(StringComparer.Ordinal); if (m_TierPassSets == null) return result; for (var i = 0; i < m_TierPassSets.Length; i++) { var set = m_TierPassSets[i]; if (set == null || set.tier != tier || set.allowedPassFeatures == null) continue; for (var k = 0; k < set.allowedPassFeatures.Length; k++) { var passFeature = set.allowedPassFeatures[k]; if (NBShaderFeatureLevelCatalog.IsManagedPassFeature(passFeature)) result.Add(passFeature); } } return result; } private static int ToTierIndex(NBShaderFeatureTier tier) { var index = (int)tier; return index >= 0 && index < 4 ? index : (int)NBShaderFeatureTier.Ultra; } private NBShaderFeatureTierKeywordSet FindTierKeywordSet(NBShaderFeatureTier tier) { if (m_TierKeywordSets == null) return null; for (var i = 0; i < m_TierKeywordSets.Length; i++) { var set = m_TierKeywordSets[i]; if (set != null && set.tier == tier) return set; } return null; } private NBShaderFeatureTierPassSet FindTierPassSet(NBShaderFeatureTier tier) { if (m_TierPassSets == null) return null; for (var i = 0; i < m_TierPassSets.Length; i++) { var set = m_TierPassSets[i]; if (set != null && set.tier == tier) return set; } return null; } private bool NormalizeQualityMappingsWithCurrentQualityLevels() { var names = QualitySettings.names; if (names == null || names.Length == 0) names = DefaultQualityNames; var previous = new Dictionary(StringComparer.Ordinal); if (m_QualityTierMappings != null) { for (var i = 0; i < m_QualityTierMappings.Length; i++) { var mapping = m_QualityTierMappings[i]; if (mapping == null || string.IsNullOrEmpty(mapping.qualityName) || previous.ContainsKey(mapping.qualityName)) continue; previous.Add(mapping.qualityName, mapping.tier); } } var changed = m_QualityTierMappings == null || m_QualityTierMappings.Length != names.Length; m_QualityTierMappings = new NBShaderQualityTierMapping[names.Length]; for (var i = 0; i < names.Length; i++) { NBShaderFeatureTier tier; if (!previous.TryGetValue(names[i], out tier)) { tier = GuessTierForQualityIndex(i, names.Length); changed = true; } m_QualityTierMappings[i] = new NBShaderQualityTierMapping { qualityName = names[i], tier = tier }; } return changed; } private static HashSet BuildSanitizedAllowedSet(string[] keywords) { var result = new HashSet(StringComparer.Ordinal); if (keywords == null) return result; for (var i = 0; i < keywords.Length; i++) { var keyword = keywords[i]; if (NBShaderFeatureLevelCatalog.IsManagedKeyword(keyword)) result.Add(keyword); } return result; } private static HashSet BuildSanitizedAllowedPassFeatureSet(string[] passFeatures) { var result = new HashSet(StringComparer.Ordinal); if (passFeatures == null) return result; for (var i = 0; i < passFeatures.Length; i++) { var passFeature = passFeatures[i]; if (NBShaderFeatureLevelCatalog.IsManagedPassFeature(passFeature)) result.Add(passFeature); } return result; } private static string[] ToCatalogOrderedArray(HashSet keywords) { if (keywords == null || keywords.Count == 0) return new string[0]; var result = new List(); var catalog = NBShaderFeatureLevelCatalog.ManagedKeywords; for (var i = 0; i < catalog.Length; i++) { if (keywords.Contains(catalog[i])) result.Add(catalog[i]); } return result.ToArray(); } private static string[] ToCatalogOrderedPassFeatureArray(HashSet passFeatures) { if (passFeatures == null || passFeatures.Count == 0) return new string[0]; var result = new List(); var catalog = NBShaderFeatureLevelCatalog.ManagedPassFeatures; for (var i = 0; i < catalog.Length; i++) { if (passFeatures.Contains(catalog[i])) result.Add(catalog[i]); } return result.ToArray(); } private static string[] GetAllowedKeywords(NBShaderFeatureTierKeywordSet[] sets, NBShaderFeatureTier tier) { if (sets == null) return new string[0]; for (var i = 0; i < sets.Length; i++) { var set = sets[i]; if (set != null && set.tier == tier) return set.allowedKeywords ?? new string[0]; } return new string[0]; } private static string[] GetAllowedPassFeatures(NBShaderFeatureTierPassSet[] sets, NBShaderFeatureTier tier) { if (sets == null) return new string[0]; for (var i = 0; i < sets.Length; i++) { var set = sets[i]; if (set != null && set.tier == tier) return set.allowedPassFeatures ?? new string[0]; } return new string[0]; } private static bool AreKeywordArraysEquivalent(string[] a, string[] b) { var setA = BuildSanitizedAllowedSet(a); var setB = BuildSanitizedAllowedSet(b); return setA.SetEquals(setB); } private static bool ArePassFeatureArraysEquivalent(string[] a, string[] b) { var setA = BuildSanitizedAllowedPassFeatureSet(a); var setB = BuildSanitizedAllowedPassFeatureSet(b); return setA.SetEquals(setB); } } }