yum-slop/modular_slang

write individual HLSL modules/libraries in slang

git clone https://git.yummers.dev/yum-slop/modular_slang

yumadd c# script37f4ab9

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