yum-slop/modular_slang
write individual HLSL modules/libraries in slang
git clone https://git.yummers.dev/yum-slop/modular_slang
37f4ab9
master
1using System ; 2using System . Collections . Generic ; 3using System . Diagnostics ; 4using System . IO ; 5using System . Linq ; 6using UnityEditor ; 7using UnityEngine ; 8 9namespace ModularSlang . Editor 10{ 11internal sealed class ScriptLocator : ScriptableObject 12{ 13} 14 15internal static class ModularSlangTranslator 16{ 17private const string MenuPath = "Assets/Translate to HLSL" ; 18 19private static string s_cachedExecutablePath ; 20private static bool s_missingExecutableLogged ; 21 22[ MenuItem ( MenuPath , false , priority : 2000 )] 23private static void TranslateSelectedSlang () 24{ 25var slangAssets = GetSelectedSlangAssets () 26. Distinct ( StringComparer . OrdinalIgnoreCase ) 27. ToList (); 28 29TranslateSlangAssets ( slangAssets , interactive : true , importOutputs : false ); 30} 31 32[ MenuItem ( MenuPath , true )] 33private static bool TranslateSelectedSlangValidation () 34{ 35return GetSelectedSlangAssets (). Any (); 36} 37 38private static IEnumerable < string > GetSelectedSlangAssets () 39{ 40foreach ( var obj in Selection . objects ) 41{ 42if ( ! obj ) 43{ 44continue ; 45} 46 47var assetPath = AssetDatabase . GetAssetPath ( obj ); 48if ( string . IsNullOrEmpty ( assetPath )) 49{ 50continue ; 51} 52 53if ( assetPath . EndsWith ( ".slang" , StringComparison . OrdinalIgnoreCase )) 54{ 55yield return assetPath ; 56} 57} 58} 59 60internal static void TranslateSlangAssets ( IReadOnlyCollection < string > assetPaths , bool interactive , bool importOutputs ) 61{ 62if ( assetPaths == null ) 63{ 64return ; 65} 66 67var uniqueAssets = assetPaths 68. Where ( path=> ! string . IsNullOrWhiteSpace ( path )) 69. Distinct ( StringComparer . OrdinalIgnoreCase ) 70. ToList (); 71 72if ( uniqueAssets . Count == 0 ) 73{ 74return ; 75} 76 77if ( ! TryGetExecutablePath ( out var exePath , interactive )) 78{ 79return ; 80} 81 82var projectRoot = Path . GetFullPath ( Path . Combine ( Application . dataPath , ".." )); 83var showProgress = interactive && uniqueAssets . Count > 1 ; 84 85try 86{ 87for ( var i = 0 ; i < uniqueAssets . Count ; ++ i ) 88{ 89var assetPath = uniqueAssets [ i ]; 90if ( showProgress ) 91{ 92var progress = ( i + 1f ) / uniqueAssets . Count ; 93EditorUtility . DisplayProgressBar ( "Translating Slang" , assetPath , progress ); 94} 95 96RunTranslator ( exePath , assetPath , projectRoot , importOutputs ); 97} 98} 99finally 100{ 101if ( showProgress ) 102{ 103EditorUtility . ClearProgressBar (); 104} 105} 106 107if ( interactive ) 108{ 109AssetDatabase . Refresh (); 110} 111} 112 113private static bool TryGetExecutablePath ( out string exePath , bool interactive ) 114{ 115exePath = GetExecutablePath (); 116if ( File . Exists ( exePath )) 117{ 118s_missingExecutableLogged = false ; 119return true ; 120} 121 122var message = 123$"Could not locate modular_slang.exe next to the Scripts folder.\n\nExpected at:\n { exePath } " ; 124 125if ( interactive ) 126{ 127EditorUtility . DisplayDialog ( "Modular Slang" , message , "OK" ); 128} 129else if ( ! s_missingExecutableLogged ) 130{ 131UnityEngine . Debug . LogError ( message ); 132s_missingExecutableLogged = true ; 133} 134 135return false ; 136} 137 138private static string GetExecutablePath () 139{ 140if ( ! string . IsNullOrEmpty ( s_cachedExecutablePath )) 141{ 142return s_cachedExecutablePath ; 143} 144 145var marker = ScriptableObject . CreateInstance < ScriptLocator > (); 146try 147{ 148var markerScript = MonoScript . FromScriptableObject ( marker ); 149var assetPath = AssetDatabase . GetAssetPath ( markerScript ); 150var projectRoot = Path . GetFullPath ( Path . Combine ( Application . dataPath , ".." )); 151var scriptFullPath = Path . GetFullPath ( Path . Combine ( projectRoot , assetPath )); 152 153var scriptDir = Path . GetDirectoryName ( scriptFullPath ) ?? string . Empty ; // .../Scripts/Editor 154var scriptsDir = Path . GetDirectoryName ( scriptDir ) ?? string . Empty ; // .../Scripts 155var packageRoot = Path . GetDirectoryName ( scriptsDir ) ?? string . Empty ; // package root 156 157s_cachedExecutablePath = Path . Combine ( packageRoot , "modular_slang.exe" ); 158return s_cachedExecutablePath ; 159} 160finally 161{ 162ScriptableObject . DestroyImmediate ( marker ); 163} 164} 165 166private static void RunTranslator ( string exePath , string assetPath , string projectRoot , bool importOutput ) 167{ 168var inputFullPath = Path . GetFullPath ( Path . Combine ( projectRoot , assetPath )); 169var outputFullPath = Path . ChangeExtension ( inputFullPath , ".hlsl" ); 170var outputAssetPath = ToProjectRelativePath ( outputFullPath , projectRoot ); 171 172var startInfo = new ProcessStartInfo 173{ 174FileName = exePath , 175Arguments = QuoteIfNeeded ( inputFullPath ), 176WorkingDirectory = Path . GetDirectoryName ( exePath ) ?? projectRoot , 177UseShellExecute = false , 178RedirectStandardOutput = true , 179RedirectStandardError = true , 180CreateNoWindow = true , 181}; 182 183using ( var process = Process . Start ( startInfo )) 184{ 185if ( process == null ) 186{ 187UnityEngine . Debug . LogError ( "Failed to launch modular_slang.exe" ); 188return ; 189} 190 191var stdOut = process . StandardOutput . ReadToEnd (); 192var stdErr = process . StandardError . ReadToEnd (); 193process . WaitForExit (); 194 195if ( ! string . IsNullOrWhiteSpace ( stdOut )) 196{ 197UnityEngine . Debug . Log ( stdOut ); 198} 199 200if ( process . ExitCode != 0 ) 201{ 202var message = string . IsNullOrWhiteSpace ( stdErr ) 203? $"modular_slang.exe failed with exit code { process . ExitCode } " 204: stdErr ; 205UnityEngine . Debug . LogError ( message ); 206return ; 207} 208 209if ( ! File . Exists ( outputFullPath )) 210{ 211UnityEngine . Debug . LogWarning ( $"Compilation succeeded but { outputFullPath } was not created." ); 212} 213else 214{ 215UnityEngine . Debug . Log ( $"Generated { outputFullPath } " ); 216if ( importOutput && ! string . IsNullOrEmpty ( outputAssetPath )) 217{ 218AssetDatabase . ImportAsset ( outputAssetPath , ImportAssetOptions . ForceUpdate ); 219} 220} 221} 222} 223 224private static string QuoteIfNeeded ( string path ) 225{ 226return path . Contains ( ' ' ) ? $"\" { path } \"" : path ; 227} 228 229private static string ToProjectRelativePath ( string fullPath , string projectRoot ) 230{ 231if ( string . IsNullOrEmpty ( fullPath ) || string . IsNullOrEmpty ( projectRoot )) 232{ 233return string . Empty ; 234} 235 236if ( ! fullPath . StartsWith ( projectRoot , StringComparison . OrdinalIgnoreCase )) 237{ 238return string . Empty ; 239} 240 241var relative = fullPath . Substring ( projectRoot . Length ) 242. TrimStart ( Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar ); 243return relative . Replace ( Path . DirectorySeparatorChar , '/' ); 244} 245} 246 247internal sealed class SlangAssetPostprocessor : AssetPostprocessor 248{ 249private static readonly HashSet < string > s_pendingAssets = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ); 250private static bool s_processingScheduled ; 251 252private static void OnPostprocessAllAssets ( 253string [] importedAssets , 254string [] deletedAssets , 255string [] movedAssets , 256string [] movedFromAssetPaths ) 257{ 258var added = false ; 259 260foreach ( var asset in importedAssets ) 261{ 262if ( asset . EndsWith ( ".slang" , StringComparison . OrdinalIgnoreCase )) 263{ 264if ( s_pendingAssets . Add ( asset )) 265{ 266added = true ; 267} 268} 269} 270 271if ( added && ! s_processingScheduled ) 272{ 273s_processingScheduled = true ; 274EditorApplication . delayCall += ProcessPending ; 275} 276} 277 278private static void ProcessPending () 279{ 280s_processingScheduled = false ; 281 282if ( s_pendingAssets . Count == 0 ) 283{ 284return ; 285} 286 287var assets = s_pendingAssets . ToArray (); 288s_pendingAssets . Clear (); 289 290ModularSlangTranslator . TranslateSlangAssets ( assets , interactive : false , importOutputs : true ); 291} 292} 293}