using System; using System.IO; using System.Text; using NBShaderEditor; using NBShaders2.Editor.FeatureLevel; using UnityEditor; using UnityEngine; namespace NBShaders2.Editor { [InitializeOnLoad] internal static class NBShaderDebugSymbolsSettingsProvider { internal const string DebugIncludeAssetPath = "Packages/com.xuanxuan.nb.fx/NBShaders2/Shader/HLSL/NBShaderDebugPragmas.hlsl"; private const string ShaderAssetPath = "Packages/com.xuanxuan.nb.fx/NBShaders2/Shader/NBShader.shader"; private const string PackageJsonAssetPath = "Packages/com.xuanxuan.nb.fx/package.json"; private const string PackageRootAssetPath = "Packages/com.xuanxuan.nb.fx/"; private static readonly Encoding Utf8NoBom = new UTF8Encoding(false); private const string DisabledIncludeContent = "// NBShader2 shader compiler debug switches.\n" + "// This file is intentionally included with #include_with_pragmas.\n" + "\n" + "#undef NB_SHADER_DEBUG_SYMBOLS\n" + "#define NB_SHADER_DEBUG_SYMBOLS 0\n"; private const string EnabledIncludeContent = "// NBShader2 shader compiler debug switches.\n" + "// This file is intentionally included with #include_with_pragmas.\n" + "\n" + "#undef NB_SHADER_DEBUG_SYMBOLS\n" + "#define NB_SHADER_DEBUG_SYMBOLS 1\n" + "#pragma enable_d3d11_debug_symbols\n"; private enum IncludeDebugState { Missing, Disabled, Enabled } static NBShaderDebugSymbolsSettingsProvider() { NBFXProjectSettings.RegisterSettingsSection( "NBShaderDebugSymbols", () => new GUIContent(Text("debugSymbols.providerLabel", "NBShader2 Debug Symbols")), OnGUI, new[] { "NBShader", "Debug", "Symbols", "D3D11", "enable_d3d11_debug_symbols" }, 90); } private static void OnGUI(string searchContext) { var settings = NBShaderFeatureLevelProjectSettings.instance; settings.EnsureInitialized(); var includeState = GetIncludeDebugState(); var includeEnabled = includeState == IncludeDebugState.Enabled; EditorGUILayout.HelpBox( Text( "debugSymbols.help.message", "Controls NBShader2 shader compiler debug symbols by rewriting the package include used by NBShader.shader. Enable only while debugging external shader tools."), MessageType.Info); DrawCachingShaderPreprocessorState(); DrawStateRows(settings.enableDebugSymbols, includeState); if (settings.enableDebugSymbols != includeEnabled || includeState == IncludeDebugState.Missing) { EditorGUILayout.HelpBox( Text( "debugSymbols.includeMismatch.message", "The ProjectSettings value and package include content do not match. Apply the current setting to resync the shader compiler input."), MessageType.Warning); if (GUILayout.Button(ButtonContent("debugSymbols.applySetting", "Apply Current Setting To Include"))) ApplySettingToInclude(settings, settings.enableDebugSymbols); } var enabled = settings.enableDebugSymbols; var toggleChanged = false; using (new EditorGUILayout.HorizontalScope()) { EditorGUI.BeginChangeCheck(); enabled = EditorGUILayout.Toggle( Content( "debugSymbols.enable", "Enable NBShader2 Debug Symbols", "Writes #pragma enable_d3d11_debug_symbols into the NBShader2 debug pragma include."), settings.enableDebugSymbols); toggleChanged = EditorGUI.EndChangeCheck(); if (GUILayout.Button(ButtonContent("debugSymbols.pingInclude", "Select Debug Toggle Include"), GUILayout.Width(180f))) PingIncludeAsset(); } if (toggleChanged) { if (!enabled || ConfirmEnableDebugSymbols()) ApplySettingToInclude(settings, enabled); } EditorGUILayout.HelpBox( Text( "debugSymbols.dirtyWarning.message", "Enabling this option modifies NBShaderDebugPragmas.hlsl in the NB_FX package. Do not commit the enabled include unless shader debug symbols are deliberately required."), MessageType.Warning); } internal static bool WriteDebugInclude(bool enabled, out string error) { error = null; string physicalPath; if (!TryGetPhysicalPath(DebugIncludeAssetPath, out physicalPath, out error)) return false; var desiredContent = enabled ? EnabledIncludeContent : DisabledIncludeContent; try { var folder = Path.GetDirectoryName(physicalPath); if (!string.IsNullOrEmpty(folder) && !Directory.Exists(folder)) Directory.CreateDirectory(folder); if (File.Exists(physicalPath) && string.Equals(File.ReadAllText(physicalPath), desiredContent, StringComparison.Ordinal)) { ReimportShaderAssets(); return true; } File.WriteAllText(physicalPath, desiredContent, Utf8NoBom); ReimportShaderAssets(); return true; } catch (Exception ex) { error = ex.Message; return false; } } private static void DrawCachingShaderPreprocessorState() { var enabled = EditorSettings.cachingShaderPreprocessor; EditorGUILayout.LabelField( Content( "debugSymbols.cachingPreprocessor", "Caching Shader Preprocessor", "#include_with_pragmas requires the Caching Shader Preprocessor."), new GUIContent(enabled ? Text("debugSymbols.status.enabled", "Enabled") : Text("debugSymbols.status.disabled", "Disabled"))); if (enabled) return; EditorGUILayout.HelpBox( Text( "debugSymbols.cachingPreprocessorWarning.message", "#include_with_pragmas requires the Caching Shader Preprocessor. Enable it before using NBShader2 debug symbols."), MessageType.Warning); if (GUILayout.Button(ButtonContent("debugSymbols.enableCachingPreprocessor", "Enable Caching Shader Preprocessor"))) { EditorSettings.cachingShaderPreprocessor = true; ReimportShaderAssets(); } } private static void DrawStateRows(bool settingsEnabled, IncludeDebugState includeState) { EditorGUILayout.LabelField( Content("debugSymbols.settingsState", "ProjectSettings State", "Saved state in ProjectSettings/NBShaderFeatureLevels.asset."), new GUIContent(settingsEnabled ? Text("debugSymbols.status.enabled", "Enabled") : Text("debugSymbols.status.disabled", "Disabled"))); EditorGUILayout.LabelField( Content("debugSymbols.includeState", "Include File State", "Actual package include content used by NBShader.shader."), new GUIContent(GetIncludeStateText(includeState))); } private static void ApplySettingToInclude(NBShaderFeatureLevelProjectSettings settings, bool enabled) { Undo.RecordObject(settings, Text("debugSymbols.undo.changeSetting", "Change NBShader2 Debug Symbols")); string error; if (WriteDebugInclude(enabled, out error)) { if (settings.enableDebugSymbols != enabled) { settings.SetDebugSymbolsEnabled(enabled); settings.SaveDebugSymbolsProjectSettings(); } UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); return; } EditorUtility.DisplayDialog( Text("debugSymbols.applyFailed.title", "Apply NBShader2 Debug Symbols Failed"), Text("debugSymbols.applyFailed.message", "Could not write NBShaderDebugPragmas.hlsl: ") + error, Text("debugSymbols.dialog.ok", "OK")); } private static bool ConfirmEnableDebugSymbols() { return EditorUtility.DisplayDialog( Text("debugSymbols.confirmEnable.title", "Enable NBShader2 Debug Symbols"), Text( "debugSymbols.confirmEnable.message", "This writes #pragma enable_d3d11_debug_symbols into the NB_FX package include. Shader size can increase and optimizations are disabled while it is enabled."), Text("debugSymbols.dialog.enable", "Enable"), Text("debugSymbols.dialog.cancel", "Cancel")); } private static IncludeDebugState GetIncludeDebugState() { string physicalPath; string error; if (!TryGetPhysicalPath(DebugIncludeAssetPath, out physicalPath, out error) || !File.Exists(physicalPath)) return IncludeDebugState.Missing; try { return ContainsActiveDebugPragma(File.ReadAllText(physicalPath)) ? IncludeDebugState.Enabled : IncludeDebugState.Disabled; } catch { return IncludeDebugState.Missing; } } private static bool ContainsActiveDebugPragma(string content) { if (string.IsNullOrEmpty(content)) return false; using (var reader = new StringReader(content)) { string line; while ((line = reader.ReadLine()) != null) { var trimmed = line.TrimStart(); if (trimmed.StartsWith("//", StringComparison.Ordinal)) continue; if (trimmed.StartsWith("#pragma", StringComparison.Ordinal) && trimmed.IndexOf("enable_d3d11_debug_symbols", StringComparison.Ordinal) >= 0) { return true; } } } return false; } private static bool TryGetPhysicalPath(string assetPath, out string physicalPath, out string error) { physicalPath = null; error = null; var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(assetPath); if (packageInfo == null) packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(PackageJsonAssetPath); if (packageInfo == null || string.IsNullOrEmpty(packageInfo.resolvedPath)) { error = "Could not resolve package path for " + assetPath; return false; } if (!assetPath.StartsWith(PackageRootAssetPath, StringComparison.Ordinal)) { error = "Asset path is outside NB_FX package: " + assetPath; return false; } var relativePath = assetPath.Substring(PackageRootAssetPath.Length).Replace('/', Path.DirectorySeparatorChar); physicalPath = Path.Combine(packageInfo.resolvedPath, relativePath); return true; } private static void ReimportShaderAssets() { AssetDatabase.ImportAsset(DebugIncludeAssetPath, ImportAssetOptions.ForceUpdate); AssetDatabase.ImportAsset(ShaderAssetPath, ImportAssetOptions.ForceUpdate); } private static void PingIncludeAsset() { var asset = AssetDatabase.LoadMainAssetAtPath(DebugIncludeAssetPath); if (asset == null) return; EditorGUIUtility.PingObject(asset); Selection.activeObject = asset; } private static string GetIncludeStateText(IncludeDebugState state) { switch (state) { case IncludeDebugState.Enabled: return Text("debugSymbols.status.enabled", "Enabled"); case IncludeDebugState.Disabled: return Text("debugSymbols.status.disabled", "Disabled"); default: return Text("debugSymbols.status.missing", "Missing"); } } private static GUIContent Content(string key, string fallback, string tip) { return NBShaderInspectorLocalization.MakeInspectorContent(key, fallback, tip); } private static GUIContent ButtonContent(string key, string fallback) { return NBShaderInspectorLocalization.MakeContent("inspector." + key + ".button", fallback); } private static string Text(string key, string fallback) { return NBShaderInspectorLocalization.GetInspectorText(key, fallback); } } }