/* ShaderTargetCompileTool.cs */ /* Place this file in Assets/Editor/ */ using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; public class ShaderTargetCompileTool : EditorWindow { /* We validate shaders against the selected build target */ private BuildTarget target = BuildTarget.PSP2; /* We keep generated probe assets isolated and disposable */ private string tempRoot = "Assets/__ShaderCompileProbe"; private string bundleName = "shader_compile_probe"; /* We write a clean shader error report after the compile probe runs */ private string reportPath = "Temp/ShaderCompileProbeReport.txt"; private bool generateErrorReport = true; private bool reportAvailable = false; /* We control compile coverage */ private bool includeVariants = true; private bool scrapeShaderPragmas = true; private bool includeUnityBuiltInPragmas = true; private bool includeVertexLightmapSafetyCombos = true; private bool exhaustiveMode = false; /* We cap generated variants in practical mode so compile time does not explode */ private int maxGeneratedCombosPerShader = 256; private Vector2 scroll; private readonly List log = new List(); /* We keep explicit known-good safety-net combos here. These are not the main variant source anymore; they are extra protection for Vita lighting cases. */ private readonly string[][] vertexLightmapSafetyCombos = { new string[] { "VERTEXLIGHT_ON", "LIGHTMAP_ON" }, new string[] { "VERTEXLIGHT_ON", "LIGHTMAP_ON", "DIRLIGHTMAP_COMBINED" }, new string[] { "VERTEXLIGHT_ON", "LIGHTMAP_ON", "_NORMALMAP" }, new string[] { "VERTEXLIGHT_ON", "LIGHTMAP_ON", "_METALLICGLOSSMAP" }, new string[] { "VERTEXLIGHT_ON", "LIGHTMAP_ON", "_NORMALMAP", "_METALLICGLOSSMAP" }, new string[] { "LIGHTMAP_ON", "_NORMALMAP" }, new string[] { "LIGHTMAP_ON", "_METALLICGLOSSMAP" }, new string[] { "LIGHTMAP_ON", "_NORMALMAP", "_METALLICGLOSSMAP" }, new string[] { "VERTEXLIGHT_ON", "_NORMALMAP" }, new string[] { "VERTEXLIGHT_ON", "_METALLICGLOSSMAP" }, new string[] { "VERTEXLIGHT_ON", "_NORMALMAP", "_METALLICGLOSSMAP" } }; /* We compile across common pass types. Unsupported pass/shader pairs are skipped safely. */ private readonly PassType[] commonPasses = { PassType.Normal, PassType.ForwardBase, PassType.ForwardAdd, PassType.ShadowCaster, PassType.Meta, PassType.Vertex, PassType.Deferred, PassType.LightPrePassBase, PassType.LightPrePassFinal }; [MenuItem("Tools/Vita/Compile Project Shaders")] public static void Open() { GetWindow("Shader Compile Probe"); } private void OnEnable() { reportAvailable = File.Exists(reportPath); } private void OnGUI() { target = (BuildTarget)EditorGUILayout.EnumPopup("Build Target", target); EditorGUILayout.Space(); includeVariants = EditorGUILayout.Toggle("Create Variant Collection", includeVariants); scrapeShaderPragmas = EditorGUILayout.Toggle("Scrape Shader Pragmas", scrapeShaderPragmas); includeUnityBuiltInPragmas = EditorGUILayout.Toggle("Expand Unity Built-In Pragmas", includeUnityBuiltInPragmas); includeVertexLightmapSafetyCombos = EditorGUILayout.Toggle("Include Vertex/Lightmap Safety Combos", includeVertexLightmapSafetyCombos); exhaustiveMode = EditorGUILayout.Toggle("Exhaustive Mode", exhaustiveMode); generateErrorReport = EditorGUILayout.Toggle("Generate Error Report", generateErrorReport); if (!exhaustiveMode) { maxGeneratedCombosPerShader = EditorGUILayout.IntField( "Max Combos / Shader", maxGeneratedCombosPerShader ); if (maxGeneratedCombosPerShader < 1) maxGeneratedCombosPerShader = 1; } EditorGUILayout.Space(); if (GUILayout.Button("Compile Project Shaders For Target", GUILayout.Height(32))) CompileShaders(); EditorGUILayout.Space(); GUI.enabled = reportAvailable && File.Exists(reportPath); if (GUILayout.Button("Open Shader Compile Report", GUILayout.Height(26))) OpenShaderCompileReport(); GUI.enabled = true; EditorGUILayout.Space(); scroll = EditorGUILayout.BeginScrollView(scroll); for (int i = 0; i < log.Count; i++) EditorGUILayout.LabelField(log[i], EditorStyles.wordWrappedLabel); EditorGUILayout.EndScrollView(); } private void CompileShaders() { log.Clear(); reportAvailable = false; long editorLogStart = 0; if (generateErrorReport) editorLogStart = GetEditorLogLength(); try { CleanupTempAssets(); Directory.CreateDirectory(tempRoot); AssetDatabase.Refresh(); string[] shaderGuids = AssetDatabase.FindAssets("t:Shader"); int materialCount = 0; int variantCount = 0; ShaderVariantCollection svc = null; if (includeVariants) svc = new ShaderVariantCollection(); for (int i = 0; i < shaderGuids.Length; i++) { string shaderPath = AssetDatabase.GUIDToAssetPath(shaderGuids[i]); if (shaderPath.StartsWith("Packages/")) continue; Shader shader = AssetDatabase.LoadAssetAtPath(shaderPath); if (shader == null) continue; /* We create one dummy material per shader. This forces Unity to include the shader in the temporary AssetBundle compile. */ Material mat = new Material(shader); string safeName = MakeSafeAssetName(shader.name); string matPath = tempRoot + "/" + safeName + ".mat"; AssetDatabase.CreateAsset(mat, matPath); AssetImporter importer = AssetImporter.GetAtPath(matPath); importer.assetBundleName = bundleName; materialCount++; log.Add("Queued shader: " + shaderPath); if (includeVariants && svc != null) variantCount += AddVariantsForShader(svc, shader, shaderPath); } if (includeVariants && svc != null) { string svcPath = tempRoot + "/GeneratedShaderVariants.shadervariants"; AssetDatabase.CreateAsset(svc, svcPath); AssetImporter importer = AssetImporter.GetAtPath(svcPath); importer.assetBundleName = bundleName; log.Add(""); log.Add("Generated ShaderVariantCollection variants: " + variantCount); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); string outputPath = "Temp/ShaderCompileProbeBundles"; if (!Directory.Exists(outputPath)) Directory.CreateDirectory(outputPath); log.Add(""); log.Add("Building temporary AssetBundle for: " + target); log.Add("Materials generated: " + materialCount); /* We use StrictMode so shader errors fail loudly. This makes the probe useful as a pre-build validation step. */ BuildAssetBundleOptions options = BuildAssetBundleOptions.ForceRebuildAssetBundle | BuildAssetBundleOptions.StrictMode; AssetBundleManifest manifest = BuildPipeline.BuildAssetBundles( outputPath, options, target ); log.Add(""); if (manifest != null) log.Add("Shader compile probe completed. Check Console / report for errors."); else log.Add("AssetBundle build failed. Check Console / report for shader errors."); } finally { if (generateErrorReport) { string newLogText = ReadEditorLogFrom(editorLogStart); WriteShaderCompileReport(newLogText); reportAvailable = File.Exists(reportPath); log.Add(""); log.Add("Shader compile report written to:"); log.Add(reportPath); } CleanupTempAssets(); AssetDatabase.Refresh(); Repaint(); } } private int AddVariantsForShader(ShaderVariantCollection svc, Shader shader, string shaderPath) { int added = 0; List combos = new List(); if (scrapeShaderPragmas) { ShaderVariantScanResult scan = ScanShaderAndIncludes(shaderPath); combos.AddRange(BuildKeywordCombos(scan)); log.Add(" Pragmas found: " + scan.keywordGroups.Count + " groups, " + scan.skipKeywords.Count + " skipped keywords"); log.Add(" Generated pragma combos: " + combos.Count); } if (includeVertexLightmapSafetyCombos) { for (int i = 0; i < vertexLightmapSafetyCombos.Length; i++) combos.Add(vertexLightmapSafetyCombos[i]); } combos = DeduplicateCombos(combos); for (int p = 0; p < commonPasses.Length; p++) { PassType passType = commonPasses[p]; added += TryAddVariant(svc, shader, passType, new string[0]); for (int c = 0; c < combos.Count; c++) added += TryAddVariant(svc, shader, passType, combos[c]); } return added; } private ShaderVariantScanResult ScanShaderAndIncludes(string shaderPath) { ShaderVariantScanResult result = new ShaderVariantScanResult(); HashSet visited = new HashSet(); ScanFileRecursive(shaderPath, result, visited); return result; } private void ScanFileRecursive( string assetPath, ShaderVariantScanResult result, HashSet visited ) { if (string.IsNullOrEmpty(assetPath)) return; if (visited.Contains(assetPath)) return; visited.Add(assetPath); string fullPath = Path.GetFullPath(assetPath); if (!File.Exists(fullPath)) return; string directory = Path.GetDirectoryName(assetPath); string[] lines = File.ReadAllLines(fullPath); bool inBlockComment = false; for (int i = 0; i < lines.Length; i++) { string line = StripComments(lines[i], ref inBlockComment).Trim(); if (string.IsNullOrEmpty(line)) continue; if (line.StartsWith("#include")) { string includePath = ExtractIncludePath(line); if (!string.IsNullOrEmpty(includePath)) { string resolved = ResolveIncludeAssetPath(directory, includePath); if (!string.IsNullOrEmpty(resolved)) ScanFileRecursive(resolved, result, visited); } continue; } if (!line.StartsWith("#pragma")) continue; ParsePragmaLine(line, result); } } private void ParsePragmaLine(string line, ShaderVariantScanResult result) { string[] tokens = SplitTokens(line); if (tokens.Length < 2) return; string pragmaType = tokens[1]; if (pragmaType == "skip_variants") { for (int i = 2; i < tokens.Length; i++) { if (!string.IsNullOrEmpty(tokens[i])) result.skipKeywords.Add(tokens[i]); } return; } if (includeUnityBuiltInPragmas && IsUnityBuiltInVariantPragma(pragmaType)) { AddUnityBuiltInKeywordGroups(pragmaType, result); return; } if (!IsExplicitVariantPragma(pragmaType)) return; List keywords = new List(); for (int i = 2; i < tokens.Length; i++) { string token = tokens[i]; if (string.IsNullOrEmpty(token)) continue; if (token == "_") continue; if (IsIgnoredPragmaToken(token)) continue; keywords.Add(token); } if (keywords.Count > 0) result.keywordGroups.Add(keywords.ToArray()); } private List BuildKeywordCombos(ShaderVariantScanResult scan) { List combos = new List(); /* We add single-keyword variants first. This gives useful coverage even when capped mode stops full expansion early. */ for (int g = 0; g < scan.keywordGroups.Count; g++) { for (int k = 0; k < scan.keywordGroups[g].Length; k++) { string keyword = scan.keywordGroups[g][k]; if (!scan.skipKeywords.Contains(keyword)) combos.Add(new string[] { keyword }); } } List generated = new List(); BuildKeywordCrossProduct(scan.keywordGroups, scan.skipKeywords, 0, new List(), generated); for (int i = 0; i < generated.Count; i++) { combos.Add(generated[i]); if (!exhaustiveMode && combos.Count >= maxGeneratedCombosPerShader) break; } return DeduplicateCombos(combos); } private void BuildKeywordCrossProduct( List groups, HashSet skipKeywords, int groupIndex, List current, List output ) { if (!exhaustiveMode && output.Count >= maxGeneratedCombosPerShader) return; if (groupIndex >= groups.Count) { if (current.Count > 0) output.Add(current.ToArray()); return; } /* We include the no-keyword option for each group. This approximates shader_feature and keeps multi_compile coverage practical. */ BuildKeywordCrossProduct(groups, skipKeywords, groupIndex + 1, current, output); string[] group = groups[groupIndex]; for (int i = 0; i < group.Length; i++) { string keyword = group[i]; if (skipKeywords.Contains(keyword)) continue; current.Add(keyword); BuildKeywordCrossProduct(groups, skipKeywords, groupIndex + 1, current, output); current.RemoveAt(current.Count - 1); if (!exhaustiveMode && output.Count >= maxGeneratedCombosPerShader) return; } } private bool IsExplicitVariantPragma(string pragmaType) { return pragmaType == "multi_compile" || pragmaType == "multi_compile_local" || pragmaType == "shader_feature" || pragmaType == "shader_feature_local"; } private bool IsUnityBuiltInVariantPragma(string pragmaType) { return pragmaType == "multi_compile_fwdbase" || pragmaType == "multi_compile_fwdadd" || pragmaType == "multi_compile_fwdadd_fullshadows" || pragmaType == "multi_compile_shadowcaster" || pragmaType == "multi_compile_fog"; } private void AddUnityBuiltInKeywordGroups(string pragmaType, ShaderVariantScanResult result) { if (pragmaType == "multi_compile_fwdbase") { result.keywordGroups.Add(new string[] { "DIRECTIONAL" }); result.keywordGroups.Add(new string[] { "LIGHTMAP_ON" }); result.keywordGroups.Add(new string[] { "DIRLIGHTMAP_COMBINED" }); result.keywordGroups.Add(new string[] { "DYNAMICLIGHTMAP_ON" }); result.keywordGroups.Add(new string[] { "VERTEXLIGHT_ON" }); result.keywordGroups.Add(new string[] { "SHADOWS_SCREEN", "SHADOWS_DEPTH" }); return; } if (pragmaType == "multi_compile_fwdadd") { result.keywordGroups.Add(new string[] { "POINT", "DIRECTIONAL", "SPOT" }); return; } if (pragmaType == "multi_compile_fwdadd_fullshadows") { result.keywordGroups.Add(new string[] { "POINT", "DIRECTIONAL", "SPOT" }); result.keywordGroups.Add(new string[] { "SHADOWS_DEPTH", "SHADOWS_CUBE", "SHADOWS_SCREEN" }); return; } if (pragmaType == "multi_compile_shadowcaster") { result.keywordGroups.Add(new string[] { "SHADOWS_DEPTH", "SHADOWS_CUBE" }); return; } if (pragmaType == "multi_compile_fog") { result.keywordGroups.Add(new string[] { "FOG_LINEAR", "FOG_EXP", "FOG_EXP2" }); return; } } private bool IsIgnoredPragmaToken(string token) { if (token.StartsWith("//")) return true; if (token == "nolightmap") return true; if (token == "nodirlightmap") return true; if (token == "nodynlightmap") return true; if (token == "noforwardadd") return true; if (token == "novertexlight") return true; if (token == "noambient") return true; if (token == "interpolateview") return true; if (token == "halfasview") return true; if (token == "addshadow") return true; if (token == "fullforwardshadows") return true; return false; } private List DeduplicateCombos(List combos) { List result = new List(); HashSet seen = new HashSet(); for (int i = 0; i < combos.Count; i++) { string[] combo = NormalizeCombo(combos[i]); string key = string.Join(" ", combo); if (string.IsNullOrEmpty(key)) continue; if (seen.Contains(key)) continue; seen.Add(key); result.Add(combo); } return result; } private string[] NormalizeCombo(string[] combo) { List clean = new List(); HashSet seen = new HashSet(); for (int i = 0; i < combo.Length; i++) { string keyword = combo[i]; if (string.IsNullOrEmpty(keyword)) continue; if (keyword == "_") continue; if (seen.Contains(keyword)) continue; seen.Add(keyword); clean.Add(keyword); } clean.Sort(); return clean.ToArray(); } private int TryAddVariant( ShaderVariantCollection svc, Shader shader, PassType passType, string[] keywords ) { try { ShaderVariantCollection.ShaderVariant variant = new ShaderVariantCollection.ShaderVariant(shader, passType, keywords); if (!svc.Contains(variant)) { svc.Add(variant); return 1; } } catch { /* Some shaders do not support certain passes or keyword combinations. We skip those so the probe keeps moving. */ } return 0; } private long GetEditorLogLength() { string path = GetEditorLogPath(); if (string.IsNullOrEmpty(path)) return 0; if (!File.Exists(path)) return 0; FileInfo info = new FileInfo(path); return info.Length; } private string ReadEditorLogFrom(long startOffset) { string path = GetEditorLogPath(); if (string.IsNullOrEmpty(path)) return ""; if (!File.Exists(path)) return ""; using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { if (startOffset < 0) startOffset = 0; if (startOffset > stream.Length) startOffset = 0; stream.Seek(startOffset, SeekOrigin.Begin); using (StreamReader reader = new StreamReader(stream)) return reader.ReadToEnd(); } } private string GetEditorLogPath() { #if UNITY_EDITOR_WIN return Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), "Unity/Editor/Editor.log" ); #elif UNITY_EDITOR_OSX return Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "Library/Logs/Unity/Editor.log" ); #else return ""; #endif } private void WriteShaderCompileReport(string logText) { string reportDirectory = Path.GetDirectoryName(reportPath); if (!string.IsNullOrEmpty(reportDirectory) && !Directory.Exists(reportDirectory)) Directory.CreateDirectory(reportDirectory); List report = new List(); report.Add("Shader Compile Probe Report"); report.Add("Target: " + target); report.Add("Generated: " + System.DateTime.Now); report.Add(""); if (string.IsNullOrEmpty(logText)) { report.Add("No new Editor.log text was captured during shader compile probe."); File.WriteAllLines(reportPath, report.ToArray()); return; } string[] lines = logText.Split('\n'); string lastProgram = ""; string lastPass = ""; string lastKeywords = ""; string lastPlatformDefines = ""; int errorCount = 0; for (int i = 0; i < lines.Length; i++) { string line = lines[i].Trim(); if (line.StartsWith("Compiling ")) lastProgram = line; if (line.StartsWith("Pass")) lastPass = line; if (line.StartsWith("Keywords:")) lastKeywords = line.Substring("Keywords:".Length).Trim(); if (line.StartsWith("Platform defines:")) lastPlatformDefines = line.Substring("Platform defines:".Length).Trim(); if (IsShaderErrorLine(line)) { errorCount++; report.Add("------------------------------------------------------------"); report.Add("Error #" + errorCount); report.Add(""); report.Add(line); if (!string.IsNullOrEmpty(lastProgram)) report.Add("Program: " + lastProgram); if (!string.IsNullOrEmpty(lastPass)) report.Add("Pass: " + lastPass); if (!string.IsNullOrEmpty(lastKeywords)) report.Add("Keywords: " + lastKeywords); if (!string.IsNullOrEmpty(lastPlatformDefines)) report.Add("Platform Defines: " + lastPlatformDefines); report.Add(""); report.Add("Context:"); /* We include nearby compiler lines because Unity often places the actual failed expression, source line, or platform compiler message nearby. */ for (int c = i + 1; c < lines.Length && c < i + 12; c++) { string context = lines[c].Trim(); if (string.IsNullOrEmpty(context)) continue; if (IsShaderErrorLine(context) && c != i) break; report.Add(context); } report.Add(""); } } if (errorCount == 0) report.Add("No shader compile errors were detected in the captured Editor.log section."); File.WriteAllLines(reportPath, report.ToArray()); } private bool IsShaderErrorLine(string line) { if (line.StartsWith("Shader error in")) return true; if (line.IndexOf("CGPROGRAM", System.StringComparison.OrdinalIgnoreCase) >= 0 && line.IndexOf("error", System.StringComparison.OrdinalIgnoreCase) >= 0) return true; if (line.IndexOf("program", System.StringComparison.OrdinalIgnoreCase) >= 0 && line.IndexOf("error", System.StringComparison.OrdinalIgnoreCase) >= 0) return true; return false; } private void OpenShaderCompileReport() { if (!File.Exists(reportPath)) { EditorUtility.DisplayDialog( "Shader Compile Report", "No shader compile report exists yet. Run the compile probe first.", "OK" ); reportAvailable = false; Repaint(); return; } EditorUtility.OpenWithDefaultApp(reportPath); } private string ExtractIncludePath(string line) { int firstQuote = line.IndexOf("\""); int lastQuote = line.LastIndexOf("\""); if (firstQuote >= 0 && lastQuote > firstQuote) return line.Substring(firstQuote + 1, lastQuote - firstQuote - 1); int firstAngle = line.IndexOf("<"); int lastAngle = line.LastIndexOf(">"); if (firstAngle >= 0 && lastAngle > firstAngle) return line.Substring(firstAngle + 1, lastAngle - firstAngle - 1); return null; } private string ResolveIncludeAssetPath(string currentDirectory, string includePath) { string relativePath = NormalizeAssetPath(Path.Combine(currentDirectory, includePath)); if (File.Exists(Path.GetFullPath(relativePath))) return relativePath; string directPath = NormalizeAssetPath(includePath); if (File.Exists(Path.GetFullPath(directPath))) return directPath; return null; } private string NormalizeAssetPath(string path) { return path.Replace("\\", "/"); } private string StripComments(string line, ref bool inBlockComment) { string output = ""; int i = 0; while (i < line.Length) { if (inBlockComment) { int end = line.IndexOf("*/", i); if (end < 0) return output; inBlockComment = false; i = end + 2; continue; } int lineComment = line.IndexOf("//", i); int blockComment = line.IndexOf("/*", i); if (lineComment >= 0 && (blockComment < 0 || lineComment < blockComment)) { output += line.Substring(i, lineComment - i); return output; } if (blockComment >= 0) { output += line.Substring(i, blockComment - i); inBlockComment = true; i = blockComment + 2; continue; } output += line.Substring(i); return output; } return output; } private string[] SplitTokens(string line) { return line.Split( new char[] { ' ', '\t' }, System.StringSplitOptions.RemoveEmptyEntries ); } private string MakeSafeAssetName(string input) { string output = input; output = output.Replace("/", "_"); output = output.Replace("\\", "_"); output = output.Replace(":", "_"); output = output.Replace("*", "_"); output = output.Replace("?", "_"); output = output.Replace("\"", "_"); output = output.Replace("<", "_"); output = output.Replace(">", "_"); output = output.Replace("|", "_"); return output; } private void CleanupTempAssets() { if (AssetDatabase.IsValidFolder(tempRoot)) AssetDatabase.DeleteAsset(tempRoot); string outputPath = "Temp/ShaderCompileProbeBundles"; if (Directory.Exists(outputPath)) Directory.Delete(outputPath, true); } /* We keep scan output grouped so future expansion can track variants per pass/subshader. For now, we aggregate groups globally per shader. */ private class ShaderVariantScanResult { public readonly List keywordGroups = new List(); public readonly HashSet skipKeywords = new HashSet(); } }