summaryrefslogtreecommitdiffstats
path: root/Scripts
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2025-10-11 18:29:21 -0700
committeryum <yum.food.vr@gmail.com>2025-10-11 18:36:32 -0700
commit37f4ab96e7b01fd1ae238243001afe0662e0a7eb (patch)
tree5e44ef44f6757bd2755d9d64d87501a1540d1f84 /Scripts
parent6e937f0998acfd1ebcf73466f74b15c0f22510a9 (diff)
add c# script
Diffstat (limited to 'Scripts')
-rw-r--r--Scripts/Editor/ModularSlang.cs293
1 files changed, 293 insertions, 0 deletions
diff --git a/Scripts/Editor/ModularSlang.cs b/Scripts/Editor/ModularSlang.cs
new file mode 100644
index 0000000..f8ae834
--- /dev/null
+++ b/Scripts/Editor/ModularSlang.cs
@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+
+namespace ModularSlang.Editor
+{
+ internal sealed class ScriptLocator : ScriptableObject
+ {
+ }
+
+ internal static class ModularSlangTranslator
+ {
+ private const string MenuPath = "Assets/Translate to HLSL";
+
+ private static string s_cachedExecutablePath;
+ private static bool s_missingExecutableLogged;
+
+ [MenuItem(MenuPath, false, priority: 2000)]
+ private static void TranslateSelectedSlang()
+ {
+ var slangAssets = GetSelectedSlangAssets()
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ TranslateSlangAssets(slangAssets, interactive: true, importOutputs: false);
+ }
+
+ [MenuItem(MenuPath, true)]
+ private static bool TranslateSelectedSlangValidation()
+ {
+ return GetSelectedSlangAssets().Any();
+ }
+
+ private static IEnumerable<string> GetSelectedSlangAssets()
+ {
+ foreach (var obj in Selection.objects)
+ {
+ if (!obj)
+ {
+ continue;
+ }
+
+ var assetPath = AssetDatabase.GetAssetPath(obj);
+ if (string.IsNullOrEmpty(assetPath))
+ {
+ continue;
+ }
+
+ if (assetPath.EndsWith(".slang", StringComparison.OrdinalIgnoreCase))
+ {
+ yield return assetPath;
+ }
+ }
+ }
+
+ internal static void TranslateSlangAssets(IReadOnlyCollection<string> assetPaths, bool interactive, bool importOutputs)
+ {
+ if (assetPaths == null)
+ {
+ return;
+ }
+
+ var uniqueAssets = assetPaths
+ .Where(path => !string.IsNullOrWhiteSpace(path))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ if (uniqueAssets.Count == 0)
+ {
+ return;
+ }
+
+ if (!TryGetExecutablePath(out var exePath, interactive))
+ {
+ return;
+ }
+
+ var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
+ var showProgress = interactive && uniqueAssets.Count > 1;
+
+ try
+ {
+ for (var i = 0; i < uniqueAssets.Count; ++i)
+ {
+ var assetPath = uniqueAssets[i];
+ if (showProgress)
+ {
+ var progress = (i + 1f) / uniqueAssets.Count;
+ EditorUtility.DisplayProgressBar("Translating Slang", assetPath, progress);
+ }
+
+ RunTranslator(exePath, assetPath, projectRoot, importOutputs);
+ }
+ }
+ finally
+ {
+ if (showProgress)
+ {
+ EditorUtility.ClearProgressBar();
+ }
+ }
+
+ if (interactive)
+ {
+ AssetDatabase.Refresh();
+ }
+ }
+
+ private static bool TryGetExecutablePath(out string exePath, bool interactive)
+ {
+ exePath = GetExecutablePath();
+ if (File.Exists(exePath))
+ {
+ s_missingExecutableLogged = false;
+ return true;
+ }
+
+ var message =
+ $"Could not locate modular_slang.exe next to the Scripts folder.\n\nExpected at:\n{exePath}";
+
+ if (interactive)
+ {
+ EditorUtility.DisplayDialog("Modular Slang", message, "OK");
+ }
+ else if (!s_missingExecutableLogged)
+ {
+ UnityEngine.Debug.LogError(message);
+ s_missingExecutableLogged = true;
+ }
+
+ return false;
+ }
+
+ private static string GetExecutablePath()
+ {
+ if (!string.IsNullOrEmpty(s_cachedExecutablePath))
+ {
+ return s_cachedExecutablePath;
+ }
+
+ var marker = ScriptableObject.CreateInstance<ScriptLocator>();
+ try
+ {
+ var markerScript = MonoScript.FromScriptableObject(marker);
+ var assetPath = AssetDatabase.GetAssetPath(markerScript);
+ var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
+ var scriptFullPath = Path.GetFullPath(Path.Combine(projectRoot, assetPath));
+
+ var scriptDir = Path.GetDirectoryName(scriptFullPath) ?? string.Empty; // .../Scripts/Editor
+ var scriptsDir = Path.GetDirectoryName(scriptDir) ?? string.Empty; // .../Scripts
+ var packageRoot = Path.GetDirectoryName(scriptsDir) ?? string.Empty; // package root
+
+ s_cachedExecutablePath = Path.Combine(packageRoot, "modular_slang.exe");
+ return s_cachedExecutablePath;
+ }
+ finally
+ {
+ ScriptableObject.DestroyImmediate(marker);
+ }
+ }
+
+ private static void RunTranslator(string exePath, string assetPath, string projectRoot, bool importOutput)
+ {
+ var inputFullPath = Path.GetFullPath(Path.Combine(projectRoot, assetPath));
+ var outputFullPath = Path.ChangeExtension(inputFullPath, ".hlsl");
+ var outputAssetPath = ToProjectRelativePath(outputFullPath, projectRoot);
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = exePath,
+ Arguments = QuoteIfNeeded(inputFullPath),
+ WorkingDirectory = Path.GetDirectoryName(exePath) ?? projectRoot,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ };
+
+ using (var process = Process.Start(startInfo))
+ {
+ if (process == null)
+ {
+ UnityEngine.Debug.LogError("Failed to launch modular_slang.exe");
+ return;
+ }
+
+ var stdOut = process.StandardOutput.ReadToEnd();
+ var stdErr = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ if (!string.IsNullOrWhiteSpace(stdOut))
+ {
+ UnityEngine.Debug.Log(stdOut);
+ }
+
+ if (process.ExitCode != 0)
+ {
+ var message = string.IsNullOrWhiteSpace(stdErr)
+ ? $"modular_slang.exe failed with exit code {process.ExitCode}"
+ : stdErr;
+ UnityEngine.Debug.LogError(message);
+ return;
+ }
+
+ if (!File.Exists(outputFullPath))
+ {
+ UnityEngine.Debug.LogWarning($"Compilation succeeded but {outputFullPath} was not created.");
+ }
+ else
+ {
+ UnityEngine.Debug.Log($"Generated {outputFullPath}");
+ if (importOutput && !string.IsNullOrEmpty(outputAssetPath))
+ {
+ AssetDatabase.ImportAsset(outputAssetPath, ImportAssetOptions.ForceUpdate);
+ }
+ }
+ }
+ }
+
+ private static string QuoteIfNeeded(string path)
+ {
+ return path.Contains(' ') ? $"\"{path}\"" : path;
+ }
+
+ private static string ToProjectRelativePath(string fullPath, string projectRoot)
+ {
+ if (string.IsNullOrEmpty(fullPath) || string.IsNullOrEmpty(projectRoot))
+ {
+ return string.Empty;
+ }
+
+ if (!fullPath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Empty;
+ }
+
+ var relative = fullPath.Substring(projectRoot.Length)
+ .TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ return relative.Replace(Path.DirectorySeparatorChar, '/');
+ }
+ }
+
+ internal sealed class SlangAssetPostprocessor : AssetPostprocessor
+ {
+ private static readonly HashSet<string> s_pendingAssets = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ private static bool s_processingScheduled;
+
+ private static void OnPostprocessAllAssets(
+ string[] importedAssets,
+ string[] deletedAssets,
+ string[] movedAssets,
+ string[] movedFromAssetPaths)
+ {
+ var added = false;
+
+ foreach (var asset in importedAssets)
+ {
+ if (asset.EndsWith(".slang", StringComparison.OrdinalIgnoreCase))
+ {
+ if (s_pendingAssets.Add(asset))
+ {
+ added = true;
+ }
+ }
+ }
+
+ if (added && !s_processingScheduled)
+ {
+ s_processingScheduled = true;
+ EditorApplication.delayCall += ProcessPending;
+ }
+ }
+
+ private static void ProcessPending()
+ {
+ s_processingScheduled = false;
+
+ if (s_pendingAssets.Count == 0)
+ {
+ return;
+ }
+
+ var assets = s_pendingAssets.ToArray();
+ s_pendingAssets.Clear();
+
+ ModularSlangTranslator.TranslateSlangAssets(assets, interactive: false, importOutputs: true);
+ }
+ }
+}