LLM for Unity  v3.0.3
Create characters in Unity with LLMs!
Loading...
Searching...
No Matches
LLMUnitySetup.cs
Go to the documentation of this file.
1
3using UnityEditor;
4using System.IO;
5using UnityEngine;
6using System.Threading.Tasks;
7using System;
8using System.IO.Compression;
9using System.Collections.Generic;
10using UnityEngine.Networking;
11using System.Text.RegularExpressions;
12using UndreamAI.LlamaLib;
13
16namespace LLMUnity
17{
19 public sealed class FloatAttribute : PropertyAttribute
20 {
21 public float Min { get; private set; }
22 public float Max { get; private set; }
23
24 public FloatAttribute(float min, float max)
25 {
26 Min = min;
27 Max = max;
28 }
29 }
30 public sealed class IntAttribute : PropertyAttribute
31 {
32 public int Min { get; private set; }
33 public int Max { get; private set; }
34
35 public IntAttribute(int min, int max)
36 {
37 Min = min;
38 Max = max;
39 }
40 }
41
42 [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
43 public class DynamicRangeAttribute : PropertyAttribute
44 {
45 public readonly string minVariable;
46 public readonly string maxVariable;
47 public bool intOrFloat;
48
49 public DynamicRangeAttribute(string minVariable, string maxVariable, bool intOrFloat)
50 {
51 this.minVariable = minVariable;
52 this.maxVariable = maxVariable;
53 this.intOrFloat = intOrFloat;
54 }
55 }
56
57 public class LLMAttribute : PropertyAttribute {}
58 public class LocalRemoteAttribute : PropertyAttribute {}
59 public class RemoteAttribute : PropertyAttribute {}
60 public class LocalAttribute : PropertyAttribute {}
61 public class ModelAttribute : PropertyAttribute {}
62 public class ModelExtrasAttribute : PropertyAttribute {}
63 public class ChatAttribute : PropertyAttribute {}
64 public class LLMUnityAttribute : PropertyAttribute {}
65
66 public class AdvancedAttribute : PropertyAttribute {}
67 public class LLMAdvancedAttribute : AdvancedAttribute {}
68 public class ModelAdvancedAttribute : AdvancedAttribute {}
69 public class ChatAdvancedAttribute : AdvancedAttribute {}
70 public class Overflow1Attribute : PropertyAttribute {}
71 public class Overflow2Attribute : PropertyAttribute {}
72
73
74 public class LLMUnityException : Exception
75 {
76 public LLMUnityException(string message = "") : base(message) {}
77 }
78
79 [Serializable]
80 public struct StringPair
81 {
82 public string source;
83 public string target;
84 }
85
86 [Serializable]
87 public class ListStringPair
88 {
89 public List<StringPair> pairs;
90 }
92
97 public class LLMUnitySetup
98 {
99 // DON'T CHANGE! the version is autocompleted with a GitHub action
101 public static string Version = "v3.0.3";
103 public static string LlamaLibVersion = "v2.0.5";
105 public static string LlamaLibReleaseURL = $"https://github.com/undreamai/LlamaLib/releases/download/{LlamaLibVersion}";
107 public static string libraryName = $"LlamaLib-{LlamaLibVersion}";
109 public static string libraryPath = GetAssetPath(libraryName);
111 public static string LlamaLibURL = $"{LlamaLibReleaseURL}/{libraryName}.zip";
113 public static string LLMUnityStore = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LLMUnity");
115 public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models");
117 public static string cacheDownloadPath = Path.Combine(LLMUnityStore, "cache");
118 public static string cacheZipPath = Path.Combine(cacheDownloadPath, Path.GetFileName(LlamaLibURL));
119 public static string cacheZipHashPath = cacheZipPath + ".sha256";
121 public static string LLMManagerPath = GetAssetPath("LLMManager.json");
122
124 [HideInInspector]
125 public static readonly Dictionary<string, (string, string, string)[]> modelOptions = new Dictionary<string, (string, string, string)[]>()
126 {
127 {"Large models (more than 10B)", new(string, string, string)[]
128 {
129 ("Gemma 3 12B", "https://huggingface.co/lmstudio-community/gemma-3-12b-it-GGUF/resolve/main/gemma-3-12b-it-Q4_K_M.gguf", "https://ai.google.dev/gemma/terms"),
130 ("Phi 4 14B", "https://huggingface.co/bartowski/phi-4-GGUF/resolve/main/phi-4-Q4_K_M.gguf", null),
131 ("Qwen 3 14B", "https://huggingface.co/unsloth/Qwen3-14B-GGUF/resolve/main/Qwen3-14B-Q4_K_M.gguf", null),
132 ("DeepSeek R1 Distill Qwen 14B", "https://huggingface.co/lmstudio-community/DeepSeek-R1-Distill-Qwen-14B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-14B-Q4_K_M.gguf", null),
133 }},
134 {"Medium models (up to 10B)", new(string, string, string)[]
135 {
136 ("Qwen 3.5 9B", "https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf", null),
137 ("Llama 3.1 8B", "https://huggingface.co/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF/resolve/main/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf", "https://huggingface.co/meta-llama/Meta-Llama-3.1-8B/blob/main/LICENSE"),
138 ("DeepSeek R1 Distill Llama 8B", "https://huggingface.co/lmstudio-community/DeepSeek-R1-Distill-Llama-8B-GGUF/resolve/main/DeepSeek-R1-Distill-Llama-8B-Q4_K_M.gguf", null),
139 ("DeepSeek R1 Distill Qwen 7B", "https://huggingface.co/lmstudio-community/DeepSeek-R1-Distill-Qwen-7B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-7B-Q4_K_M.gguf", null),
140 ("Gemma 2 9B it", "https://huggingface.co/bartowski/gemma-2-9b-it-GGUF/resolve/main/gemma-2-9b-it-Q4_K_M.gguf", "https://ai.google.dev/gemma/terms"),
141 ("Mistral 7B Instruct v0.2", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf", null),
142 ("OpenHermes 2.5 7B", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf", null),
143 }},
144 {"Small models (up to 5B)", new(string, string, string)[]
145 {
146 ("Qwen 3.5 4B", "https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf", null),
147 ("Llama 3.2 3B", "https://huggingface.co/hugging-quants/Llama-3.2-3B-Instruct-Q4_K_M-GGUF/resolve/main/llama-3.2-3b-instruct-q4_k_m.gguf", "https://huggingface.co/meta-llama/Llama-3.2-1B/blob/main/LICENSE.txt"),
148 ("Gemma 3 4B", "https://huggingface.co/lmstudio-community/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it-Q4_K_M.gguf", "https://ai.google.dev/gemma/terms"),
149 ("Phi 4 4B", "https://huggingface.co/bartowski/microsoft_Phi-4-mini-instruct-GGUF/resolve/main/microsoft_Phi-4-mini-instruct-Q4_K_M.gguf", null),
150 }},
151 {"Tiny models (up to 2B)", new(string, string, string)[]
152 {
153 ("Qwen 3.5 2B", "https://huggingface.co/unsloth/Qwen3.5-2B-GGUF/resolve/main/Qwen3.5-2B-Q4_K_M.gguf", null),
154 ("Qwen 3.5 0.8B", "https://huggingface.co/unsloth/Qwen3.5-0.8B-GGUF/resolve/main/Qwen3.5-0.8B-Q4_K_M.gguf", null),
155 ("Llama 3.2 1B", "https://huggingface.co/hugging-quants/Llama-3.2-1B-Instruct-Q4_K_M-GGUF/resolve/main/llama-3.2-1b-instruct-q4_k_m.gguf", "https://huggingface.co/meta-llama/Llama-3.2-1B/blob/main/LICENSE.txt"),
156 ("Gemma 3 1B", "https://huggingface.co/lmstudio-community/gemma-3-1b-it-GGUF/resolve/main/gemma-3-1b-it-Q4_K_M.gguf", "https://ai.google.dev/gemma/terms"),
157 ("DeepSeek R1 Distill Qwen 1.5B", "https://huggingface.co/lmstudio-community/DeepSeek-R1-Distill-Qwen-1.5B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-1.5B-Q4_K_M.gguf", null),
158 }},
159 {"RAG models", new(string, string, string)[]
160 {
161 ("All MiniLM L12 v2", "https://huggingface.co/leliuga/all-MiniLM-L12-v2-GGUF/resolve/main/all-MiniLM-L12-v2.Q4_K_M.gguf", null),
162 ("BGE large en v1.5", "https://huggingface.co/CompendiumLabs/bge-large-en-v1.5-gguf/resolve/main/bge-large-en-v1.5-q4_k_m.gguf", null),
163 ("BGE base en v1.5", "https://huggingface.co/CompendiumLabs/bge-base-en-v1.5-gguf/resolve/main/bge-base-en-v1.5-q4_k_m.gguf", null),
164 ("BGE small en v1.5", "https://huggingface.co/CompendiumLabs/bge-small-en-v1.5-gguf/resolve/main/bge-small-en-v1.5-q4_k_m.gguf", null),
165 }},
166 };
167
169 [LLMUnity] public static DebugModeType DebugMode = DebugModeType.All;
170 static string DebugModeKey = "DebugMode";
171 public static bool CUBLAS = false;
172 static string CUBLASKey = "CUBLAS";
173 static List<Action<string>> errorCallbacks = new List<Action<string>>();
174 static readonly object lockObject = new object();
175 static Dictionary<string, Task> androidExtractTasks = new Dictionary<string, Task>();
176
177 public enum DebugModeType
178 {
179 Debug,
180 All,
181 Warning,
182 Error,
183 None
184 }
185
186 public static void Log(string message)
187 {
188 if ((int)DebugMode > (int)DebugModeType.All) return;
189 Debug.Log(message);
190 }
191
192 public static void LogWarning(string message)
193 {
194 if ((int)DebugMode > (int)DebugModeType.Warning) return;
195 Debug.LogWarning(message);
196 }
197
198 public static void LogError(string message, bool throwException = false)
199 {
200 if ((int)DebugMode <= (int)DebugModeType.Error || throwException)
201 {
202 Debug.LogError(message);
203 foreach (Action<string> errorCallback in errorCallbacks) errorCallback(message);
204 }
205 if (throwException) throw new LLMUnityException(message);
206 }
207
208 static void LoadPlayerPrefs()
209 {
210 DebugMode = (DebugModeType)PlayerPrefs.GetInt(DebugModeKey, (int)DebugModeType.All);
211 CUBLAS = PlayerPrefs.GetInt(CUBLASKey, 0) == 1;
212 }
213
214 public static void SetDebugMode(DebugModeType newDebugMode)
215 {
216 if (DebugMode == newDebugMode) return;
217 DebugMode = newDebugMode;
218 PlayerPrefs.SetInt(DebugModeKey, (int)DebugMode);
219 PlayerPrefs.Save();
220 }
221
222#if UNITY_EDITOR
223 public static void SetCUBLAS(bool value)
224 {
225 if (CUBLAS == value) return;
226 CUBLAS = value;
227 PlayerPrefs.SetInt(CUBLASKey, value ? 1 : 0);
228 PlayerPrefs.Save();
229 }
230
231#endif
232
233 public static string GetAssetPath(string relPath = "")
234 {
235 string assetsDir = Application.platform == RuntimePlatform.Android ? Application.persistentDataPath : Application.streamingAssetsPath;
236 return Path.Combine(assetsDir, relPath).Replace('\\', '/');
237 }
238
239 public static string GetDownloadAssetPath(string relPath = "")
240 {
241 string assetsDir = Application.streamingAssetsPath;
242
243 bool isVisionOS = false;
244#if UNITY_2022_3_OR_NEWER
245 isVisionOS = Application.platform == RuntimePlatform.VisionOS;
246#endif
247 if (Application.platform == RuntimePlatform.Android || Application.platform == RuntimePlatform.IPhonePlayer || isVisionOS)
248 {
249 assetsDir = Application.persistentDataPath;
250 }
251 return Path.Combine(assetsDir, relPath).Replace('\\', '/');
252 }
253
254 static void InitializeOnLoadCommon()
255 {
256#if UNITY_EDITOR || !((UNITY_ANDROID || UNITY_IOS || UNITY_VISIONOS))
257 LlamaLib.baseLibraryPath = Path.Combine(libraryPath, LlamaLib.GetPlatform(), "native");
258#endif
259 }
260
261#if UNITY_EDITOR
262 [InitializeOnLoadMethod]
263 static async Task InitializeOnLoad()
264 {
265 LoadPlayerPrefs();
266 LlamaLib.libraryExclusion = new List<string>(){CUBLAS ? "tinyblas" : "cublas"};
267 InitializeOnLoadCommon();
268 await DownloadLibrary();
269 }
270
271#else
272 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
273 static void InitializeOnLoad()
274 {
275 InitializeOnLoadCommon();
276 }
277
278#endif
279
280 static Dictionary<string, ResumingWebClient> downloadClients = new Dictionary<string, ResumingWebClient>();
281
282 public static void CancelDownload(string savePath)
283 {
284 if (!downloadClients.ContainsKey(savePath)) return;
285 downloadClients[savePath].CancelDownloadAsync();
286 downloadClients.Remove(savePath);
287 }
288
289 public static async Task DownloadFile(
290 string fileUrl, string savePath, bool overwrite = false,
291 Action<string> callback = null, Action<float> progressCallback = null, bool debug = true
292 )
293 {
294 if (File.Exists(savePath) && !overwrite)
295 {
296 if(debug) Log($"File already exists at: {savePath}");
297 }
298 else
299 {
300 if(debug) Log($"Downloading {fileUrl} to {savePath}...");
301 string tmpPath = Path.Combine(Application.temporaryCachePath, Path.GetFileName(savePath));
302
303 ResumingWebClient client = new ResumingWebClient();
304 downloadClients[savePath] = client;
305 if (File.Exists(tmpPath) && overwrite) File.Delete(tmpPath);
306 await client.DownloadFileTaskAsyncResume(new Uri(fileUrl), tmpPath, !overwrite, progressCallback);
307 downloadClients.Remove(savePath);
308#if UNITY_EDITOR
309 AssetDatabase.StartAssetEditing();
310#endif
311 Directory.CreateDirectory(Path.GetDirectoryName(savePath));
312 if (File.Exists(savePath)) File.Delete(savePath);
313 File.Move(tmpPath, savePath);
314#if UNITY_EDITOR
315 AssetDatabase.StopAssetEditing();
316#endif
317 if(debug) Log($"Download complete!");
318 }
319
320 progressCallback?.Invoke(1f);
321 callback?.Invoke(savePath);
322 }
323
324 public static async Task AndroidExtractFile(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024 * 1024)
325 {
326 Task extractionTask;
327 lock (lockObject)
328 {
329 if (!androidExtractTasks.TryGetValue(assetName, out extractionTask))
330 {
331#if !UNITY_EDITOR && UNITY_ANDROID
332 extractionTask = AndroidExtractFileOnce(assetName, overwrite, log, chunkSize);
333#else
334 extractionTask = Task.CompletedTask;
335#endif
336 androidExtractTasks[assetName] = extractionTask;
337 }
338 }
339 await extractionTask;
340 }
341
342 public static async Task AndroidExtractFileOnce(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024 * 1024)
343 {
344 string source = "jar:file://" + Application.dataPath + "!/assets/" + assetName;
345 string target = GetAssetPath(assetName);
346 if (!overwrite && File.Exists(target))
347 {
348 if (log) Log($"File {target} already exists");
349 return;
350 }
351
352 Log($"Extracting {source} to {target}");
353
354 // UnityWebRequest to read the file from StreamingAssets
355 UnityWebRequest www = UnityWebRequest.Get(source);
356 // Send the request and await its completion
357 var operation = www.SendWebRequest();
358
359 while (!operation.isDone) await Task.Delay(1);
360 if (www.result != UnityWebRequest.Result.Success)
361 {
362 LogError("Failed to load file from StreamingAssets: " + www.error);
363 }
364 else
365 {
366 byte[] buffer = new byte[chunkSize];
367 using (Stream responseStream = new MemoryStream(www.downloadHandler.data))
368 using (FileStream fileStream = new FileStream(target, FileMode.Create, FileAccess.Write))
369 {
370 int bytesRead;
371 while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
372 {
373 await fileStream.WriteAsync(buffer, 0, bytesRead);
374 }
375 }
376 }
377 }
378
379 public static async Task AndroidExtractAsset(string path, bool overwrite = false)
380 {
381 if (Application.platform != RuntimePlatform.Android) return;
382 await AndroidExtractFile(Path.GetFileName(path), overwrite);
383 }
384
385 public static string GetFullPath(string path)
386 {
387 return Path.GetFullPath(path).Replace('\\', '/');
388 }
389
390 public static bool IsSubPath(string childPath, string parentPath)
391 {
392 return GetFullPath(childPath).StartsWith(GetFullPath(parentPath), StringComparison.OrdinalIgnoreCase);
393 }
394
395 public static string RelativePath(string fullPath, string basePath)
396 {
397 // Get the full paths and replace backslashes with forward slashes (or vice versa)
398 string fullParentPath = GetFullPath(basePath).TrimEnd('/');
399 string fullChildPath = GetFullPath(fullPath);
400
401 string relativePath = fullChildPath;
402 if (fullChildPath.StartsWith(fullParentPath, StringComparison.OrdinalIgnoreCase))
403 {
404 relativePath = fullChildPath.Substring(fullParentPath.Length);
405 while (relativePath.StartsWith("/")) relativePath = relativePath.Substring(1);
406 }
407 return relativePath;
408 }
409
410 public static string SearchDirectory(string directory, string targetFileName)
411 {
412 string[] files = Directory.GetFiles(directory, targetFileName);
413 if (files.Length > 0) return files[0];
414 string[] subdirectories = Directory.GetDirectories(directory);
415 foreach (var subdirectory in subdirectories)
416 {
417 string result = SearchDirectory(subdirectory, targetFileName);
418 if (result != null) return result;
419 }
420 return null;
421 }
422
423#if UNITY_EDITOR
424
425 [HideInInspector] public static float libraryProgress = 1;
426
427 public static void CreateEmptyFile(string path)
428 {
429 File.Create(path).Dispose();
430 }
431
432 static void ExtractInsideDirectory(string zipPath, string extractPath, string prefix = "", bool overwrite = true)
433 {
434 using (ZipArchive archive = ZipFile.OpenRead(zipPath))
435 {
436 foreach (ZipArchiveEntry entry in archive.Entries)
437 {
438 if (string.IsNullOrEmpty(entry.Name))
439 continue; // Skip directories
440
441 string destinationPath;
442 if (!String.IsNullOrEmpty(prefix))
443 {
444 string normalizedPath = entry.FullName.Replace('\\', '/');
445 if (!normalizedPath.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
446 continue;
447 destinationPath = Path.Combine(extractPath, normalizedPath.Substring(prefix.Length));
448 }
449 else
450 {
451 destinationPath = Path.Combine(extractPath, entry.FullName);
452 }
453
454 Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
455 entry.ExtractToFile(destinationPath, overwrite);
456 }
457 }
458 }
459
460 static async Task DownloadAndExtractInsideDirectory()
461 {
462 string setupDir = Path.Combine(libraryPath, "setup");
463 Directory.CreateDirectory(setupDir);
464
465 string setupFile = Path.Combine(setupDir, Path.GetFileName(LlamaLibURL) + ".complete");
466 if (File.Exists(setupFile)) return;
467
468 Directory.CreateDirectory(cacheDownloadPath);
469 foreach (string existingZipPath in Directory.GetFiles(cacheDownloadPath, "*.zip"))
470 {
471 if (existingZipPath != cacheZipPath)
472 {
473 File.Delete(existingZipPath);
474 }
475 }
476
477 string hashurl = LlamaLibURL + ".sha256";
478 string cacheZipNewHashPath = cacheZipHashPath + ".new";
479 string hash = File.Exists(cacheZipHashPath)? File.ReadAllText(cacheZipHashPath).Trim() : "";
480 bool same_hash = false;
481 try
482 {
483 new ResumingWebClient().GetURLFileSize(hashurl); // avoid showing error if url doesn't exist
484 await DownloadFile(hashurl, cacheZipNewHashPath, debug: false);
485 same_hash = File.ReadAllText(cacheZipNewHashPath).Trim() == hash;
486 } catch {}
487
488 if (!File.Exists(cacheZipPath) || !same_hash) await DownloadFile(LlamaLibURL, cacheZipPath, true, null, SetLibraryProgress);
489
490 AssetDatabase.StartAssetEditing();
491 ExtractInsideDirectory(cacheZipPath, libraryPath, $"{libraryName}/runtimes/");
492 CreateEmptyFile(setupFile);
493 AssetDatabase.StopAssetEditing();
494
495 if (File.Exists(cacheZipNewHashPath))
496 {
497 if (File.Exists(cacheZipHashPath)) File.Delete(cacheZipHashPath);
498 File.Move(cacheZipNewHashPath, cacheZipHashPath);
499 }
500 }
501
502 static void DeleteEarlierVersions()
503 {
504 List<string> assetPathSubDirs = new List<string>();
505 foreach (string dir in new string[] { GetAssetPath(), Path.Combine(Application.dataPath, "Plugins", "Android") })
506 {
507 if (Directory.Exists(dir)) assetPathSubDirs.AddRange(Directory.GetDirectories(dir));
508 }
509
510 List<Regex> versionRegexes = new List<Regex> { new Regex("undreamai-(.+)-llamacpp"), new Regex("LlamaLib-(.+)") };
511 foreach (string assetPathSubDir in assetPathSubDirs)
512 {
513 foreach (Regex regex in versionRegexes)
514 {
515 Match match = regex.Match(Path.GetFileName(assetPathSubDir));
516 if (match.Success)
517 {
518 string version = match.Groups[1].Value;
519 if (version != LlamaLibVersion)
520 {
521 Debug.Log($"Deleting other LLMUnity version folder: {assetPathSubDir}");
522 Directory.Delete(assetPathSubDir, true);
523 if (File.Exists(assetPathSubDir + ".meta")) File.Delete(assetPathSubDir + ".meta");
524 }
525 }
526 }
527 }
528 }
529
530 static async Task DownloadLibrary()
531 {
532 if (libraryProgress < 1) return;
533 try
534 {
535 DeleteEarlierVersions();
536 }
537 catch (Exception e)
538 {
539 LogError(e.Message);
540 }
541
542 for (int i=1; i<=3; i++)
543 {
544 if (i > 1) Log("Downloading LlamaLib failed, try #" + i);
545 libraryProgress = 0;
546 try
547 {
548 await DownloadAndExtractInsideDirectory();
549 break;
550 }
551 catch (Exception e)
552 {
553 LogError(e.Message);
554 }
555 }
556
557 libraryProgress = 1;
558 }
559
560 public static async Task RedownloadLibrary()
561 {
562 if (File.Exists(cacheZipPath)) File.Delete(cacheZipPath);
563 if (File.Exists(cacheZipHashPath)) File.Delete(cacheZipHashPath);
564 if (Directory.Exists(libraryPath)) Directory.Delete(libraryPath, true);
565 await DownloadLibrary();
566 }
567
568 private static void SetLibraryProgress(float progress)
569 {
570 libraryProgress = Math.Min(0.99f, progress);
571 }
572
573 public static string AddAsset(string assetPath)
574 {
575 if (!File.Exists(assetPath))
576 {
577 LogError($"{assetPath} does not exist!");
578 return null;
579 }
580 string assetDir = GetAssetPath();
581 if (IsSubPath(assetPath, assetDir)) return RelativePath(assetPath, assetDir);
582
583 string filename = Path.GetFileName(assetPath);
584 string fullPath = GetAssetPath(filename);
585 AssetDatabase.StartAssetEditing();
586 foreach (string path in new string[] { fullPath, fullPath + ".meta" })
587 {
588 if (File.Exists(path)) File.Delete(path);
589 }
590 File.Copy(assetPath, fullPath);
591 AssetDatabase.StopAssetEditing();
592 return filename;
593 }
594
595#endif
597
599 public static void AddErrorCallBack(Action<string> callback)
600 {
601 errorCallbacks.Add(callback);
602 }
603
605 public static void RemoveErrorCallBack(Action<string> callback)
606 {
607 errorCallbacks.Remove(callback);
608 }
609
611 public static void ClearErrorCallBacks()
612 {
613 errorCallbacks.Clear();
614 }
615
616 public static int GetMaxFreqKHz(int cpuId)
617 {
618 string[] paths = new string[]
619 {
620 $"/sys/devices/system/cpu/cpufreq/stats/cpu{cpuId}/time_in_state",
621 $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/stats/time_in_state",
622 $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/cpuinfo_max_freq"
623 };
624
625 foreach (var path in paths)
626 {
627 if (!File.Exists(path)) continue;
628
629 int maxFreqKHz = 0;
630 using (StreamReader sr = new StreamReader(path))
631 {
632 string line;
633 while ((line = sr.ReadLine()) != null)
634 {
635 string[] parts = line.Split(' ');
636 if (parts.Length > 0 && int.TryParse(parts[0], out int freqKHz))
637 {
638 if (freqKHz > maxFreqKHz)
639 {
640 maxFreqKHz = freqKHz;
641 }
642 }
643 }
644 }
645 if (maxFreqKHz != 0) return maxFreqKHz;
646 }
647 return -1;
648 }
649
650 public static bool IsSmtCpu(int cpuId)
651 {
652 string[] paths = new string[]
653 {
654 $"/sys/devices/system/cpu/cpu{cpuId}/topology/core_cpus_list",
655 $"/sys/devices/system/cpu/cpu{cpuId}/topology/thread_siblings_list"
656 };
657
658 foreach (var path in paths)
659 {
660 if (!File.Exists(path)) continue;
661 using (StreamReader sr = new StreamReader(path))
662 {
663 string line;
664 while ((line = sr.ReadLine()) != null)
665 {
666 if (line.Contains(",") || line.Contains("-"))
667 {
668 return true;
669 }
670 }
671 }
672 }
673 return false;
674 }
675
680 public static int AndroidGetNumBigCores()
681 {
682 int maxFreqKHzMin = int.MaxValue;
683 int maxFreqKHzMax = 0;
684 List<int> cpuMaxFreqKHz = new List<int>();
685 List<bool> cpuIsSmtCpu = new List<bool>();
686
687 try
688 {
689 string cpuPath = "/sys/devices/system/cpu/";
690 int coreIndex;
691 if (Directory.Exists(cpuPath))
692 {
693 foreach (string cpuDir in Directory.GetDirectories(cpuPath))
694 {
695 string dirName = Path.GetFileName(cpuDir);
696 if (!dirName.StartsWith("cpu")) continue;
697 if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue;
698
699 int maxFreqKHz = GetMaxFreqKHz(coreIndex);
700 cpuMaxFreqKHz.Add(maxFreqKHz);
701 if (maxFreqKHz > maxFreqKHzMax) maxFreqKHzMax = maxFreqKHz;
702 if (maxFreqKHz < maxFreqKHzMin) maxFreqKHzMin = maxFreqKHz;
703 cpuIsSmtCpu.Add(IsSmtCpu(coreIndex));
704 }
705 }
706 }
707 catch (Exception e)
708 {
709 LogError(e.Message);
710 }
711
712 int numBigCores = 0;
713 int numCores = SystemInfo.processorCount;
714 int maxFreqKHzMedium = (maxFreqKHzMin + maxFreqKHzMax) / 2;
715 if (maxFreqKHzMedium == maxFreqKHzMax) numBigCores = numCores;
716 else
717 {
718 for (int i = 0; i < cpuMaxFreqKHz.Count; i++)
719 {
720 if (cpuIsSmtCpu[i] || cpuMaxFreqKHz[i] >= maxFreqKHzMedium) numBigCores++;
721 }
722 }
723
724 if (numBigCores == 0) numBigCores = SystemInfo.processorCount / 2;
725 else numBigCores = Math.Min(numBigCores, SystemInfo.processorCount);
726
727 return numBigCores;
728 }
729
735 {
736 List<int> capacities = new List<int>();
737 int minCapacity = int.MaxValue;
738 try
739 {
740 string cpuPath = "/sys/devices/system/cpu/";
741 int coreIndex;
742 if (Directory.Exists(cpuPath))
743 {
744 foreach (string cpuDir in Directory.GetDirectories(cpuPath))
745 {
746 string dirName = Path.GetFileName(cpuDir);
747 if (!dirName.StartsWith("cpu")) continue;
748 if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue;
749
750 string capacityPath = Path.Combine(cpuDir, "cpu_capacity");
751 if (!File.Exists(capacityPath)) break;
752
753 int capacity = int.Parse(File.ReadAllText(capacityPath).Trim());
754 capacities.Add(capacity);
755 if (minCapacity > capacity) minCapacity = capacity;
756 }
757 }
758 }
759 catch (Exception e)
760 {
761 LogError(e.Message);
762 }
763
764 int numBigCores = 0;
765 foreach (int capacity in capacities)
766 {
767 if (capacity >= 2 * minCapacity) numBigCores++;
768 }
769
770 if (numBigCores == 0 || numBigCores > SystemInfo.processorCount) numBigCores = SystemInfo.processorCount;
771 return numBigCores;
772 }
773 }
774}
Class implementing helper functions for setup and process management.
static int AndroidGetNumBigCoresCapacity()
Calculates the number of big cores in Android similarly to Unity (https://docs.unity3d....
static string LLMManagerPath
Path of file with build information for runtime.
static string LlamaLibReleaseURL
LlamaLib release url.
static string libraryPath
LlamaLib path.
static void AddErrorCallBack(Action< string > callback)
Add callback function to call for error logs.
static void RemoveErrorCallBack(Action< string > callback)
Remove callback function added for error logs.
static void ClearErrorCallBacks()
Remove all callback function added for error logs.
static string modelDownloadPath
Model download path.
static string LlamaLibVersion
LlamaLib version.
static int AndroidGetNumBigCores()
Calculates the number of big cores in Android similarly to ncnn (https://github.com/Tencent/ncnn)
static string Version
LLM for Unity version.
static string libraryName
LlamaLib name.
static string cacheDownloadPath
cache download path
static readonly Dictionary< string,(string, string, string)[]> modelOptions
Default models for download.
static string LLMUnityStore
LLMnity store path.
static string LlamaLibURL
LlamaLib url.