diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/BywayQualityRuleData.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/BywayQualityRuleData.cs index 7d85955..623470a 100644 --- a/Packages/com.bywaystudios.qualitytuner/Runtime/BywayQualityRuleData.cs +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/BywayQualityRuleData.cs @@ -7,12 +7,31 @@ namespace Byway.Quality public class BywaySystemMemoryRuleMatcher : SystemMemoryRuleMatcherBase { } + public class BywayMaliGenerationRuleMatcher : MaliGenerationRuleMatcherBase { } + + public class BywayEmulatorDetectionRuleMatcher : EmulatorDetectionRuleMatcherBase { } + + public class BywayGraphicsApiRuleMatcher : GraphicsApiRuleMatcherBase { } + + public class BywaySocNameRuleMatcher : SocNameRuleMatcherBase { } + + public class BywayAndroidApiLevelRuleMatcher : AndroidApiLevelRuleMatcherBase { } + + public class BywayScreenResolutionRuleMatcher : ScreenResolutionRuleMatcherBase { } + + public class BywayCompositeAndRuleMatcher : CompositeAndRuleMatcherBase { } + + public class BywayCompositeOrRuleMatcher : CompositeOrRuleMatcherBase { } + + public class BywayDeviceNameContainsRuleMatcher : DeviceNameContainsRuleMatcherBase { } + public enum BywayQualityLevel { - Lowest, + VeryLow, Low, Medium, High, - Highest + VeryHigh, + Ultra } -} \ No newline at end of file +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/GraphicsApi.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/GraphicsApi.cs new file mode 100644 index 0000000..6c405a6 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/GraphicsApi.cs @@ -0,0 +1,16 @@ +namespace Byway.Quality +{ + /// + /// Graphics API category, used by GraphicsApiRuleMatcherBase to gate quality on the actual + /// rendering backend selected by Unity at runtime (Vulkan vs OpenGLES3 vs OpenGLES2). + /// + public enum GraphicsApi + { + /// Match any graphics API. Effectively skips the API check. + Any = 0, + + Vulkan = 1, + OpenGLES3 = 2, + OpenGLES2 = 3 + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/GraphicsApi.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/GraphicsApi.cs.meta new file mode 100644 index 0000000..d4916cc --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/GraphicsApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b0e8c2f4d3a5758d9c0f1a2e3b4c5d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.Android.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.Android.cs index 9c4048b..499798a 100644 --- a/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.Android.cs +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.Android.cs @@ -8,6 +8,8 @@ namespace Byway.Quality { // Build.VERSION_CODES.S (Android 12) private const int ANDROID_VERSION_CODES_S = 31; + // Build.VERSION_CODES.Q (Android 10) — getCurrentThermalStatus available + private const int ANDROID_VERSION_CODES_Q = 29; private static int _sdkVersion; @@ -30,6 +32,17 @@ namespace Byway.Quality _ => (GpuMinorSeries.Unknown, 0) }; stats.SocName = GetSocName(); + + stats.AndroidSdkInt = GetAndroidSdkVersion(); + stats.BuildManufacturer = GetBuildString("MANUFACTURER"); + stats.BuildBrand = GetBuildString("BRAND"); + stats.BuildModel = GetBuildString("MODEL"); + stats.BuildHardware = GetBuildString("HARDWARE"); + stats.BuildProduct = GetBuildString("PRODUCT"); + stats.BuildSocManufacturer = GetBuildSocManufacturer(); + stats.MaliGeneration = DeriveMaliGeneration(stats.GpuMajorSeries, stats.GpuMinorSeries, stats.GpuSeriesNumber, gpuName); + stats.IsEmulator = DetectEmulator(stats); + stats.ThermalStatus = GetCurrentThermalStatus(); } public static GpuMajorSeries ParseGpuMajorSeries(string gpuName) @@ -175,7 +188,159 @@ namespace Byway.Quality } } - private static int GetAndroidSdkVersion() + // --------------------------------------------------------------------- + // v0.3.0 additions + // --------------------------------------------------------------------- + + public static string GetBuildSocManufacturer() + { + if (GetAndroidSdkVersion() < ANDROID_VERSION_CODES_S) + return ""; + + try + { + using var buildClass = new AndroidJavaClass("android.os.Build"); + var socManufacturer = buildClass.GetStatic("SOC_MANUFACTURER"); + return socManufacturer ?? ""; + } + catch (Exception e) + { + Debug.LogException(e); + return ""; + } + } + + private static string GetBuildString(string fieldName) + { +#if UNITY_EDITOR + if (Application.isEditor) + return ""; +#endif +#if UNITY_ANDROID + try + { + using var buildClass = new AndroidJavaClass("android.os.Build"); + var value = buildClass.GetStatic(fieldName); + return value ?? ""; + } + catch (Exception e) + { + Debug.LogException(e); + return ""; + } +#else + return ""; +#endif + } + + public static MaliGeneration DeriveMaliGeneration(GpuMajorSeries major, GpuMinorSeries minor, int seriesNumber, string gpuName) + { + if (major == GpuMajorSeries.Immortalis) + return MaliGeneration.Immortalis; + + if (major != GpuMajorSeries.Mali) + return MaliGeneration.Unknown; + + // Utgard: "Mali-400/450/470" — minor parsed as Mali (no letter) + if (minor == GpuMinorSeries.Mali) + return MaliGeneration.Utgard; + + // Midgard: Mali-T6xx/T7xx/T8xx + if (minor == GpuMinorSeries.MaliT) + return MaliGeneration.Midgard; + + if (minor != GpuMinorSeries.MaliG) + return MaliGeneration.Unknown; + + // Mali-G family disambiguation by series number band: + // G31/G52/G57/G68 → BifrostLegacy (low-end Bifrost+early Valhall, all <70) + // G71/G72/G76/G77/G78 → Valhall1 (legacy Valhall, 70-99) + // G310/G510/G610/G615/G710/G715/G720/G725 → Valhall2 (modern, >=100) + if (seriesNumber >= 100) + return MaliGeneration.Valhall2; + if (seriesNumber >= 70) + return MaliGeneration.Valhall1; + if (seriesNumber > 0) + return MaliGeneration.BifrostLegacy; + + return MaliGeneration.Unknown; + } + + public static bool DetectEmulator(HardwareStats stats) + { + // x86/x86_64 CPU on Android is the strongest single signal — no shipping + // Android phone uses x86 in 2024+. Check first. + var cpu = stats.ProcessorType ?? ""; + if (cpu.IndexOf("x86", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + + var hardware = (stats.BuildHardware ?? "").ToLowerInvariant(); + if (hardware == "goldfish" || hardware == "ranchu" || + hardware.Contains("vbox") || hardware.Contains("ttvm") || + hardware.Contains("nox") || hardware == "intel") + return true; + + var manufacturer = stats.BuildManufacturer ?? ""; + if (manufacturer.Equals("unknown", StringComparison.OrdinalIgnoreCase) || + manufacturer.Equals("Genymotion", StringComparison.OrdinalIgnoreCase) || + manufacturer.Equals("BlueStacks", StringComparison.OrdinalIgnoreCase) || + manufacturer.Equals("ldplayer", StringComparison.OrdinalIgnoreCase) || + manufacturer.Equals("Netease", StringComparison.OrdinalIgnoreCase)) + return true; + + var product = (stats.BuildProduct ?? "").ToLowerInvariant(); + if (product.StartsWith("sdk_", StringComparison.Ordinal) || + product.Contains("vbox") || product.Contains("ldplayer") || + product == "nox" || product.Contains("ttvm")) + return true; + + var model = stats.BuildModel ?? ""; + if (model.IndexOf("sdk", StringComparison.OrdinalIgnoreCase) >= 0 || + model.IndexOf("Emulator", StringComparison.OrdinalIgnoreCase) >= 0 || + model.IndexOf("Android SDK built for", StringComparison.OrdinalIgnoreCase) >= 0 || + model.IndexOf("BlueStacks", StringComparison.OrdinalIgnoreCase) >= 0 || + model.IndexOf("MuMu", StringComparison.Ordinal) >= 0) + return true; + + return false; + } + + public static int GetCurrentThermalStatus() + { +#if UNITY_EDITOR + if (Application.isEditor) + return -1; +#endif +#if UNITY_ANDROID + if (GetAndroidSdkVersion() < ANDROID_VERSION_CODES_Q) + return -1; + + try + { + using var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); + using var activity = unityPlayer.GetStatic("currentActivity"); + if (activity == null) + return -1; + + using var pmObj = activity.Call("getSystemService", "power"); + if (pmObj == null) + return -1; + + return pmObj.Call("getCurrentThermalStatus"); + } + catch (Exception e) + { + Debug.LogException(e); + return -1; + } +#else + return -1; +#endif + } + + // --------------------------------------------------------------------- + + public static int GetAndroidSdkVersion() { #if UNITY_EDITOR if (Application.isEditor) diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.cs index 2117ce0..d572f7a 100644 --- a/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.cs +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.cs @@ -1,4 +1,5 @@ using UnityEngine; +using UnityEngine.Rendering; namespace Byway.Quality { @@ -53,6 +54,64 @@ namespace Byway.Quality /// public int SystemMemorySizeMb { get; internal set; } + // --------------------------------------------------------------------- + // v0.3.0 additions (additive only; existing matchers/assets unaffected). + // --------------------------------------------------------------------- + + /// Android Build$VERSION.SDK_INT; 0 on non-Android / editor. + public int AndroidSdkInt { get; internal set; } + + /// True when the device is detected as an Android emulator (BlueStacks/MuMu/LDPlayer/AVD/etc.). False on iOS and physical Android. + public bool IsEmulator { get; internal set; } + + /// . + public GraphicsDeviceType GraphicsDeviceType { get; internal set; } + + /// . + public int GraphicsShaderLevel { get; internal set; } + + /// at hardware-stats capture time. + public int ScreenWidth { get; internal set; } + + /// at hardware-stats capture time. + public int ScreenHeight { get; internal set; } + + /// at hardware-stats capture time. May be 0 if unknown. + public float ScreenDpi { get; internal set; } + + /// . + public int ProcessorCount { get; internal set; } + + /// in MHz. + public int ProcessorFrequencyMHz { get; internal set; } + + /// . Strong x86 emulator signal on Android. + public string ProcessorType { get; internal set; } + + /// Android Build.MANUFACTURER; empty on non-Android. + public string BuildManufacturer { get; internal set; } + + /// Android Build.BRAND; empty on non-Android. + public string BuildBrand { get; internal set; } + + /// Android Build.MODEL; empty on non-Android. Distinct from which is Unity's composed string. + public string BuildModel { get; internal set; } + + /// Android Build.HARDWARE; empty on non-Android. Useful emulator signal (goldfish/ranchu). + public string BuildHardware { get; internal set; } + + /// Android Build.PRODUCT; empty on non-Android. Useful emulator signal (sdk_*/vbox*). + public string BuildProduct { get; internal set; } + + /// Android Build.SOC_MANUFACTURER (API 31+); empty otherwise. + public string BuildSocManufacturer { get; internal set; } + + /// Derived Mali architectural generation; on non-Mali / non-Android. + public MaliGeneration MaliGeneration { get; internal set; } + + /// Android PowerManager.getCurrentThermalStatus() (API 29+); -1 if unsupported / not Android. + public int ThermalStatus { get; internal set; } + internal static HardwareStats CreateDefault() { return new HardwareStats @@ -64,7 +123,26 @@ namespace Byway.Quality GpuMinorSeries = GpuMinorSeries.Unknown, GpuSeriesNumber = 0, SocName = "", - SystemMemorySizeMb = SystemInfo.systemMemorySize + SystemMemorySizeMb = SystemInfo.systemMemorySize, + + AndroidSdkInt = 0, + IsEmulator = false, + GraphicsDeviceType = SystemInfo.graphicsDeviceType, + GraphicsShaderLevel = SystemInfo.graphicsShaderLevel, + ScreenWidth = Screen.width, + ScreenHeight = Screen.height, + ScreenDpi = Screen.dpi, + ProcessorCount = SystemInfo.processorCount, + ProcessorFrequencyMHz = SystemInfo.processorFrequency, + ProcessorType = SystemInfo.processorType, + BuildManufacturer = "", + BuildBrand = "", + BuildModel = "", + BuildHardware = "", + BuildProduct = "", + BuildSocManufacturer = "", + MaliGeneration = MaliGeneration.Unknown, + ThermalStatus = -1 }; } } diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.iOS.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.iOS.cs index ab8950b..ac26018 100644 --- a/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.iOS.cs +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/HardwareInfo.iOS.cs @@ -15,6 +15,7 @@ namespace Byway.Quality stats.GpuMajorSeries = GpuMajorSeries.Apple; stats.GpuMinorSeries = ParseGpuMinorSeries(stats.GpuName); stats.GpuSeriesNumber = ParseAppleGpuSeriesNumber(stats.GpuName); + stats.IsEmulator = false; } public static GpuMinorSeries ParseGpuMinorSeries(string gpuName) diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/MaliGeneration.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/MaliGeneration.cs new file mode 100644 index 0000000..27cfb0c --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/MaliGeneration.cs @@ -0,0 +1,30 @@ +namespace Byway.Quality +{ + /// + /// Generation taxonomy for ARM Mali / Immortalis GPUs. + /// Used by MaliGenerationRuleMatcherBase to disambiguate the legacy/modern Mali numeric collision + /// (e.g. Mali-G77 vs Mali-G310 — both MaliG, but very different architectures). + /// + public enum MaliGeneration + { + Unknown = 0, + + /// Mali-400 / 450 / 470 (no letter suffix). + Utgard = 1, + + /// Mali-T6xx / T7xx / T8xx. + Midgard = 2, + + /// Mali-G31 / G52 / G57 / G68 (low-end Bifrost / early Valhall band). + BifrostLegacy = 3, + + /// Mali-G71 / G72 / G76 / G77 / G78 (Valhall first wave, legacy numbering < 100). + Valhall1 = 4, + + /// Mali-G310 / G510 / G610 / G615 / G710 / G715 / G720 / G725 (modern Valhall numbering >= 100). + Valhall2 = 5, + + /// ARM Immortalis-G715 / G720 / G725 / G925 (top-tier ray-tracing line). + Immortalis = 6 + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/MaliGeneration.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/MaliGeneration.cs.meta new file mode 100644 index 0000000..a6421ac --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/MaliGeneration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f8c6a9d2e1b4536b7a8d0e1c2f3a4b5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers.meta new file mode 100644 index 0000000..6756faa --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7a4c3e9d2b1f4a5e8c6d0f2b3a4e5c7d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/AndroidApiLevelRuleMatcherBase.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/AndroidApiLevelRuleMatcherBase.cs new file mode 100644 index 0000000..d8fb789 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/AndroidApiLevelRuleMatcherBase.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Byway.Quality +{ + [Serializable] + public class AndroidApiLevelRuleMatcherBase : RuleMatcherBase + { + public Rule[] rules; + + protected override IEnumerable> Rules => rules; + + [Serializable] + public sealed class Rule : IMatcher + { + [Tooltip("Minimum Android SDK_INT (inclusive); 0 = no min")] + public int apiLevelMin; + + [Tooltip("Maximum Android SDK_INT (inclusive); 0 = no max → treated as 999")] + public int apiLevelMax; + + [Tooltip("Quality level for the device that match")] + public T qualityLevel; + + public bool TryMatch(HardwareStats stats, out T matchedQualityLevel) + { + matchedQualityLevel = default; + + if (stats.AndroidSdkInt <= 0) + return false; + + var max = apiLevelMax <= 0 ? 999 : apiLevelMax; + if (stats.AndroidSdkInt < apiLevelMin || stats.AndroidSdkInt > max) + return false; + + matchedQualityLevel = qualityLevel; + return true; + } + } + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/AndroidApiLevelRuleMatcherBase.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/AndroidApiLevelRuleMatcherBase.cs.meta new file mode 100644 index 0000000..6eb4e0e --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/AndroidApiLevelRuleMatcherBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0513c6d8e7f9a6cb1f2c3d4e5f60718 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/CompositeRuleMatcherBase.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/CompositeRuleMatcherBase.cs new file mode 100644 index 0000000..5ba1f36 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/CompositeRuleMatcherBase.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Byway.Quality +{ + [Serializable] + public class CompositeAndRuleMatcherBase : RuleMatcherBase + { + public Rule[] rules; + + protected override IEnumerable> Rules => rules; + + [Serializable] + public sealed class Rule : IMatcher + { + [Tooltip("All children must match for this rule to fire. Children's own quality levels are discarded.")] + [SerializeReference] + [SelectableSerializeReference] + public IMatcher[] children; + + [Tooltip("Quality level returned when all children match")] + public T qualityLevel; + + public bool TryMatch(HardwareStats stats, out T matchedQualityLevel) + { + matchedQualityLevel = default; + + if (children == null || children.Length == 0) + return false; + + foreach (var child in children) + { + if (child == null) + return false; + if (!child.TryMatch(stats, out _)) + return false; + } + + matchedQualityLevel = qualityLevel; + return true; + } + } + } + + [Serializable] + public class CompositeOrRuleMatcherBase : RuleMatcherBase + { + public Rule[] rules; + + protected override IEnumerable> Rules => rules; + + [Serializable] + public sealed class Rule : IMatcher + { + [Tooltip("Any one child matching causes this rule to fire. Children's own quality levels are discarded.")] + [SerializeReference] + [SelectableSerializeReference] + public IMatcher[] children; + + [Tooltip("Quality level returned when any child matches")] + public T qualityLevel; + + public bool TryMatch(HardwareStats stats, out T matchedQualityLevel) + { + matchedQualityLevel = default; + + if (children == null || children.Length == 0) + return false; + + foreach (var child in children) + { + if (child == null) + continue; + if (child.TryMatch(stats, out _)) + { + matchedQualityLevel = qualityLevel; + return true; + } + } + + return false; + } + } + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/CompositeRuleMatcherBase.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/CompositeRuleMatcherBase.cs.meta new file mode 100644 index 0000000..cc1d8f6 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/CompositeRuleMatcherBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e2735e8fa09b1c8ed3f4e5f60718293a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/DeviceNameContainsRuleMatcherBase.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/DeviceNameContainsRuleMatcherBase.cs new file mode 100644 index 0000000..d9cddcf --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/DeviceNameContainsRuleMatcherBase.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Byway.Quality +{ + [Serializable] + public class DeviceNameContainsRuleMatcherBase : RuleMatcherBase + { + public Rule[] rules; + + protected override IEnumerable> Rules => rules; + + [Serializable] + public sealed class Rule : IMatcher + { + [Tooltip("Pattern to compare against SystemInfo.deviceModel")] + public string deviceModelPattern; + + [Tooltip("How to compare the pattern against deviceModel")] + public StringMatchMode matchMode; + + [Tooltip("Quality level for the device that match")] + public T qualityLevel; + + public bool TryMatch(HardwareStats stats, out T matchedQualityLevel) + { + matchedQualityLevel = default; + + if (string.IsNullOrEmpty(stats.DeviceModel)) + return false; + if (string.IsNullOrEmpty(deviceModelPattern)) + return false; + + if (!StringMatchModeUtility.IsMatch(stats.DeviceModel, deviceModelPattern, matchMode)) + return false; + + matchedQualityLevel = qualityLevel; + return true; + } + } + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/DeviceNameContainsRuleMatcherBase.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/DeviceNameContainsRuleMatcherBase.cs.meta new file mode 100644 index 0000000..01cfe50 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/DeviceNameContainsRuleMatcherBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f3846f90b1ac2d9fe4f5061728293a4b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/EmulatorDetectionRuleMatcherBase.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/EmulatorDetectionRuleMatcherBase.cs new file mode 100644 index 0000000..e1ccae4 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/EmulatorDetectionRuleMatcherBase.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Byway.Quality +{ + [Serializable] + public class EmulatorDetectionRuleMatcherBase : RuleMatcherBase + { + public Rule[] rules; + + protected override IEnumerable> Rules => rules; + + [Serializable] + public sealed class Rule : IMatcher + { + [Tooltip("true: match if emulator detected; false: match if NOT emulator")] + public bool matchEmulator; + + [Tooltip("Quality level for the device that match")] + public T qualityLevel; + + public bool TryMatch(HardwareStats stats, out T matchedQualityLevel) + { + matchedQualityLevel = default; + if (stats.IsEmulator != matchEmulator) + return false; + + matchedQualityLevel = qualityLevel; + return true; + } + } + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/EmulatorDetectionRuleMatcherBase.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/EmulatorDetectionRuleMatcherBase.cs.meta new file mode 100644 index 0000000..d34823f --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/EmulatorDetectionRuleMatcherBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d2e0f3a5b4c6749e8d9f0a1b2c3d4e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/GraphicsApiRuleMatcherBase.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/GraphicsApiRuleMatcherBase.cs new file mode 100644 index 0000000..983d8b7 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/GraphicsApiRuleMatcherBase.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Byway.Quality +{ + [Serializable] + public class GraphicsApiRuleMatcherBase : RuleMatcherBase + { + public Rule[] rules; + + protected override IEnumerable> Rules => rules; + + [Serializable] + public sealed class Rule : IMatcher + { + [Tooltip("Required graphics API. Any = skip API check.")] + public GraphicsApi requiredApi; + + [Tooltip("Minimum SystemInfo.graphicsShaderLevel (inclusive); 0 = no min")] + public int requiredShaderLevelMin; + + [Tooltip("Quality level for the device that match")] + public T qualityLevel; + + public bool TryMatch(HardwareStats stats, out T matchedQualityLevel) + { + matchedQualityLevel = default; + + if (requiredApi != GraphicsApi.Any && !ApiMatches(requiredApi, stats.GraphicsDeviceType)) + return false; + + if (stats.GraphicsShaderLevel < requiredShaderLevelMin) + return false; + + matchedQualityLevel = qualityLevel; + return true; + } + + private static bool ApiMatches(GraphicsApi required, GraphicsDeviceType actual) + { + return required switch + { + GraphicsApi.Vulkan => actual == GraphicsDeviceType.Vulkan, + GraphicsApi.OpenGLES3 => actual == GraphicsDeviceType.OpenGLES3, + GraphicsApi.OpenGLES2 => actual == GraphicsDeviceType.OpenGLES2, + _ => true + }; + } + } + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/GraphicsApiRuleMatcherBase.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/GraphicsApiRuleMatcherBase.cs.meta new file mode 100644 index 0000000..a4b8069 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/GraphicsApiRuleMatcherBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae3f1a4b6c5d784af9e0a1b2c3d4e5f6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/MaliGenerationRuleMatcherBase.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/MaliGenerationRuleMatcherBase.cs new file mode 100644 index 0000000..df9bfa9 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/MaliGenerationRuleMatcherBase.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Byway.Quality +{ + [Serializable] + public class MaliGenerationRuleMatcherBase : RuleMatcherBase + { + public Rule[] rules; + + protected override IEnumerable> Rules => rules; + + [Serializable] + public sealed class Rule : IMatcher + { + [Tooltip("Mali architectural generation that match")] + public MaliGeneration generation; + + [Tooltip("Minimum of GPU series number that match (inclusive); 0 = no min")] + public int gpuSeriesNumberMin; + + [Tooltip("Maximum of GPU series number that match (inclusive); 0 = no max → treated as int.MaxValue")] + public int gpuSeriesNumberMax; + + [Tooltip("Quality level for the device that match")] + public T qualityLevel; + + public bool TryMatch(HardwareStats stats, out T matchedQualityLevel) + { + matchedQualityLevel = default; + + if (stats.MaliGeneration == MaliGeneration.Unknown) + return false; + if (stats.MaliGeneration != generation) + return false; + + var max = gpuSeriesNumberMax <= 0 ? int.MaxValue : gpuSeriesNumberMax; + if (stats.GpuSeriesNumber < gpuSeriesNumberMin || stats.GpuSeriesNumber > max) + return false; + + matchedQualityLevel = qualityLevel; + return true; + } + } + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/MaliGenerationRuleMatcherBase.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/MaliGenerationRuleMatcherBase.cs.meta new file mode 100644 index 0000000..d6bb018 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/MaliGenerationRuleMatcherBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c1d9e2f4a3b5648d7c8e9f0a1b2c3d4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/ScreenResolutionRuleMatcherBase.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/ScreenResolutionRuleMatcherBase.cs new file mode 100644 index 0000000..4837009 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/ScreenResolutionRuleMatcherBase.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Byway.Quality +{ + [Serializable] + public class ScreenResolutionRuleMatcherBase : RuleMatcherBase + { + public Rule[] rules; + + protected override IEnumerable> Rules => rules; + + [Serializable] + public sealed class Rule : IMatcher + { + [Tooltip("Minimum total pixel count (width*height), inclusive; 0 = no min")] + public int pixelCountMin; + + [Tooltip("Maximum total pixel count (width*height), inclusive; 0 = no max → unbounded")] + public int pixelCountMax; + + [Tooltip("Minimum DPI, inclusive; 0 = no min (DPI also skipped if Screen.dpi reports 0)")] + public float dpiMin; + + [Tooltip("Maximum DPI, inclusive; 0 = no max → unbounded")] + public float dpiMax; + + [Tooltip("Quality level for the device that match")] + public T qualityLevel; + + public bool TryMatch(HardwareStats stats, out T matchedQualityLevel) + { + matchedQualityLevel = default; + + var pixelCount = stats.ScreenWidth * stats.ScreenHeight; + var pxMax = pixelCountMax <= 0 ? int.MaxValue : pixelCountMax; + if (pixelCount < pixelCountMin || pixelCount > pxMax) + return false; + + if (stats.ScreenDpi > 0f) + { + var dMax = dpiMax <= 0f ? float.MaxValue : dpiMax; + if (stats.ScreenDpi < dpiMin || stats.ScreenDpi > dMax) + return false; + } + else if (dpiMin > 0f) + { + return false; + } + + matchedQualityLevel = qualityLevel; + return true; + } + } + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/ScreenResolutionRuleMatcherBase.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/ScreenResolutionRuleMatcherBase.cs.meta new file mode 100644 index 0000000..8a30f47 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/ScreenResolutionRuleMatcherBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d1624d7e9f8a0b7dc2f3d4e5f6071829 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/SocNameRuleMatcherBase.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/SocNameRuleMatcherBase.cs new file mode 100644 index 0000000..02e5106 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/SocNameRuleMatcherBase.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using UnityEngine; + +namespace Byway.Quality +{ + [Serializable] + public class SocNameRuleMatcherBase : RuleMatcherBase + { + public Rule[] rules; + + protected override IEnumerable> Rules => rules; + + [Serializable] + public sealed class Rule : IMatcher + { + [Tooltip("Pattern to compare against HardwareStats.SocName (Build.SOC_MODEL on Android 12+)")] + public string socNamePattern; + + [Tooltip("How to compare the pattern against SocName")] + public StringMatchMode matchMode; + + [Tooltip("Quality level for the device that match")] + public T qualityLevel; + + public bool TryMatch(HardwareStats stats, out T matchedQualityLevel) + { + matchedQualityLevel = default; + + if (string.IsNullOrEmpty(stats.SocName)) + return false; + if (string.IsNullOrEmpty(socNamePattern)) + return false; + + if (!StringMatchModeUtility.IsMatch(stats.SocName, socNamePattern, matchMode)) + return false; + + matchedQualityLevel = qualityLevel; + return true; + } + } + } + + internal static class StringMatchModeUtility + { + private static bool _regexFailureLogged; + + public static bool IsMatch(string input, string pattern, StringMatchMode mode) + { + return mode switch + { + StringMatchMode.Exact => input.Equals(pattern, StringComparison.Ordinal), + StringMatchMode.StartsWith => input.StartsWith(pattern, StringComparison.Ordinal), + StringMatchMode.Contains => input.IndexOf(pattern, StringComparison.Ordinal) >= 0, + StringMatchMode.Regex => TryRegex(input, pattern), + _ => false + }; + } + + private static bool TryRegex(string input, string pattern) + { + try + { + return Regex.IsMatch(input, pattern); + } + catch (ArgumentException e) + { + if (!_regexFailureLogged) + { + _regexFailureLogged = true; + Debug.LogWarning($"[QualityTuner] invalid regex pattern '{pattern}': {e.Message}"); + } + return false; + } + } + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/SocNameRuleMatcherBase.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/SocNameRuleMatcherBase.cs.meta new file mode 100644 index 0000000..a039029 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/Matchers/SocNameRuleMatcherBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bf402b5c7d6e895ba0f1b2c3d4e5f607 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/StringMatchMode.cs b/Packages/com.bywaystudios.qualitytuner/Runtime/StringMatchMode.cs new file mode 100644 index 0000000..e1fbf65 --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/StringMatchMode.cs @@ -0,0 +1,20 @@ +namespace Byway.Quality +{ + /// + /// String matching mode shared by SocNameRuleMatcherBase and DeviceNameContainsRuleMatcherBase. + /// + public enum StringMatchMode + { + /// Exact ordinal match. + Exact = 0, + + /// Ordinal StartsWith match. + StartsWith = 1, + + /// Ordinal Contains match. + Contains = 2, + + /// .NET regex match. Invalid patterns return false (logged once). + Regex = 3 + } +} diff --git a/Packages/com.bywaystudios.qualitytuner/Runtime/StringMatchMode.cs.meta b/Packages/com.bywaystudios.qualitytuner/Runtime/StringMatchMode.cs.meta new file mode 100644 index 0000000..71ac53e --- /dev/null +++ b/Packages/com.bywaystudios.qualitytuner/Runtime/StringMatchMode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a9d7b1e3c2f4647c8b9e0f1d2a3b4c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.bywaystudios.qualitytuner/package.json b/Packages/com.bywaystudios.qualitytuner/package.json index fd88a67..1d86153 100644 --- a/Packages/com.bywaystudios.qualitytuner/package.json +++ b/Packages/com.bywaystudios.qualitytuner/package.json @@ -1,11 +1,11 @@ { "name": "com.bywaystudios.qualitytuner", "displayName": "Quality Tuner", - "version": "0.1.0", + "version": "0.3.1", "description": "Tools to support deciding quality level by hardware spec of mobile devices.", "repository": { "url": "git@gitea.bywaystudios.com:wangshiyao/QualitySettingsPackage.git", "type": "git", "revision": null } -} +} \ No newline at end of file