initial commit
This commit is contained in:
parent
6715289efe
commit
788c3389af
37645 changed files with 2526849 additions and 80 deletions
|
@ -0,0 +1,829 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor.Build.Content;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
using UnityEditor.Build.Pipeline.Utilities.USerialize;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Assertions;
|
||||
using UnityEngine.Build.Pipeline;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of the Build Cache
|
||||
/// </summary>
|
||||
public class BuildCache : IBuildCache, IDisposable
|
||||
{
|
||||
// Custom serialization handler for 'BuildUsageTagSet' type as it cannot be correctly serialized purely with reflection
|
||||
internal class USerializeCustom_BuildUsageTagSet : ICustomSerializer
|
||||
{
|
||||
#if !UNITY_2019_4_OR_NEWER
|
||||
static MethodInfo m_SerializeToBinary = typeof(BuildUsageTagSet).GetMethod("SerializeToBinary", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
static MethodInfo m_DeserializeFromBinary = typeof(BuildUsageTagSet).GetMethod("DeserializeFromBinary", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
#endif
|
||||
|
||||
// Return the type that this custom serializer deals with
|
||||
Type ICustomSerializer.GetType()
|
||||
{
|
||||
return typeof(BuildUsageTagSet);
|
||||
}
|
||||
|
||||
void ICustomSerializer.USerializer(Serializer serializer, object value)
|
||||
{
|
||||
BuildUsageTagSet buildUsageTagSet = (BuildUsageTagSet)value;
|
||||
if (serializer.WriteNullFlag(buildUsageTagSet))
|
||||
#if UNITY_2019_4_OR_NEWER
|
||||
serializer.WriteBytes(buildUsageTagSet.SerializeToBinary());
|
||||
#else
|
||||
serializer.WriteBytes((byte[])m_SerializeToBinary.Invoke(buildUsageTagSet, null));
|
||||
#endif
|
||||
}
|
||||
|
||||
object ICustomSerializer.UDeSerializer(DeSerializer deserializer)
|
||||
{
|
||||
BuildUsageTagSet tagSet = null;
|
||||
if (deserializer.ReadNullFlag())
|
||||
{
|
||||
tagSet = new BuildUsageTagSet();
|
||||
byte[] bytes = deserializer.ReadBytes();
|
||||
#if UNITY_2019_4_OR_NEWER
|
||||
tagSet.DeserializeFromBinary(bytes);
|
||||
#else
|
||||
m_DeserializeFromBinary.Invoke(tagSet, new object[] { bytes });
|
||||
#endif
|
||||
}
|
||||
return tagSet;
|
||||
}
|
||||
}
|
||||
|
||||
// Object factories used to create instances of types involved in build cache serialization more quickly than the generic Activator.CreateInstance()
|
||||
internal static (Type, DeSerializer.ObjectFactory)[] ObjectFactories = new (Type, DeSerializer.ObjectFactory)[]
|
||||
{
|
||||
(typeof(AssetLoadInfo), () => { return new AssetLoadInfo(); }),
|
||||
(typeof(BuildUsageTagGlobal), () => { return new BuildUsageTagGlobal(); }),
|
||||
(typeof(BundleDetails), () => { return new BundleDetails(); }),
|
||||
(typeof(CachedInfo), () => { return new CachedInfo(); }),
|
||||
(typeof(CacheEntry), () => { return new CacheEntry(); }),
|
||||
(typeof(ExtendedAssetData), () => { return new ExtendedAssetData(); }),
|
||||
(typeof(ObjectIdentifier), () => { return new ObjectIdentifier(); }),
|
||||
(typeof(ObjectSerializedInfo), () => { return new ObjectSerializedInfo(); }),
|
||||
(typeof(ResourceFile), () => { return new ResourceFile(); }),
|
||||
(typeof(SceneDependencyInfo), () => { return new SceneDependencyInfo(); }),
|
||||
(typeof(SerializedFileMetaData), () => { return new SerializedFileMetaData(); }),
|
||||
(typeof(SerializedLocation), () => { return new SerializedLocation(); }),
|
||||
(typeof(SpriteImporterData), () => { return new SpriteImporterData(); }),
|
||||
(typeof(WriteResult), () => { return new WriteResult(); }),
|
||||
(typeof(KeyValuePair<ObjectIdentifier, Type[]>), () => { return new KeyValuePair<ObjectIdentifier, Type[]>(); }),
|
||||
(typeof(List<KeyValuePair<ObjectIdentifier, Type[]>>), () => { return new List<KeyValuePair<ObjectIdentifier, Type[]>>(); }),
|
||||
(typeof(Hash128), () => { return new Hash128(); }),
|
||||
};
|
||||
|
||||
// Custom serializers we use for build cache serialization of types that cannot be correctly serialized using the built in reflection based serialization code
|
||||
internal static ICustomSerializer[] CustomSerializers = new ICustomSerializer[]
|
||||
{
|
||||
new USerializeCustom_BuildUsageTagSet()
|
||||
};
|
||||
const string k_CachePath = "Library/BuildCache";
|
||||
const int k_Version = 5;
|
||||
internal const int k_CacheServerVersion = 2;
|
||||
internal const long k_BytesToGigaBytes = 1073741824L;
|
||||
|
||||
[NonSerialized]
|
||||
IBuildLogger m_Logger;
|
||||
|
||||
[NonSerialized]
|
||||
Hash128 m_GlobalHash;
|
||||
|
||||
[NonSerialized]
|
||||
CacheServerUploader m_Uploader;
|
||||
|
||||
[NonSerialized]
|
||||
CacheServerDownloader m_Downloader;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new build cache object.
|
||||
/// </summary>
|
||||
public BuildCache()
|
||||
{
|
||||
m_GlobalHash = CalculateGlobalArtifactVersionHash();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new remote build cache object.
|
||||
/// </summary>
|
||||
/// <param name="host">The server host.</param>
|
||||
/// <param name="port">The server port.</param>
|
||||
public BuildCache(string host, int port = 8126)
|
||||
{
|
||||
m_GlobalHash = CalculateGlobalArtifactVersionHash();
|
||||
|
||||
if (string.IsNullOrEmpty(host))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
m_Uploader = new CacheServerUploader(host, port);
|
||||
m_Downloader = new CacheServerDownloader(this, new DeSerializer(CustomSerializers, ObjectFactories), host, port);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_Uploader = null;
|
||||
m_Downloader = null;
|
||||
string msg = $"Failed to connect build cache to CacheServer. ip: {host}, port: {port}. With exception, \"{e.Message}\"";
|
||||
m_Logger.AddEntrySafe(LogLevel.Warning, msg);
|
||||
UnityEngine.Debug.LogWarning(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// internal for testing purposes only
|
||||
internal void OverrideGlobalHash(Hash128 hash)
|
||||
{
|
||||
m_GlobalHash = hash;
|
||||
if (m_Uploader != null)
|
||||
m_Uploader.SetGlobalHash(m_GlobalHash);
|
||||
if (m_Downloader != null)
|
||||
m_Downloader.SetGlobalHash(m_GlobalHash);
|
||||
}
|
||||
|
||||
static Hash128 CalculateGlobalArtifactVersionHash()
|
||||
{
|
||||
#if UNITY_2019_3_OR_NEWER
|
||||
return HashingMethods.Calculate(Application.unityVersion, k_Version).ToHash128();
|
||||
#else
|
||||
return HashingMethods.Calculate(PlayerSettings.scriptingRuntimeVersion, Application.unityVersion, k_Version).ToHash128();
|
||||
#endif
|
||||
}
|
||||
|
||||
internal void ClearCacheEntryMaps()
|
||||
{
|
||||
BuildCacheUtility.ClearCacheHashes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the build cache instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (m_Downloader != null)
|
||||
m_Downloader.Dispose();
|
||||
m_Uploader = null;
|
||||
m_Downloader = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CacheEntry GetCacheEntry(GUID asset, int version = 1)
|
||||
{
|
||||
return BuildCacheUtility.GetCacheEntry(asset, version);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CacheEntry GetCacheEntry(string path, int version = 1)
|
||||
{
|
||||
return BuildCacheUtility.GetCacheEntry(path, version);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CacheEntry GetCacheEntry(ObjectIdentifier objectID, int version = 1)
|
||||
{
|
||||
if (objectID.guid.Empty())
|
||||
return GetCacheEntry(objectID.filePath, version);
|
||||
return GetCacheEntry(objectID.guid, version);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CacheEntry GetCacheEntry(Type type, int version = 1)
|
||||
{
|
||||
return BuildCacheUtility.GetCacheEntry(type, version);
|
||||
}
|
||||
|
||||
internal CacheEntry GetUpdatedCacheEntry(CacheEntry entry)
|
||||
{
|
||||
if (entry.Type == CacheEntry.EntryType.File)
|
||||
return GetCacheEntry(entry.File, entry.Version);
|
||||
if (entry.Type == CacheEntry.EntryType.Asset)
|
||||
return GetCacheEntry(entry.Guid, entry.Version);
|
||||
if (entry.Type == CacheEntry.EntryType.ScriptType)
|
||||
return GetCacheEntry(Type.GetType(entry.ScriptType), entry.Version);
|
||||
return entry;
|
||||
}
|
||||
|
||||
internal bool LogCacheMiss(string msg)
|
||||
{
|
||||
if (!ScriptableBuildPipeline.logCacheMiss)
|
||||
return false;
|
||||
m_Logger.AddEntrySafe(LogLevel.Warning, msg);
|
||||
UnityEngine.Debug.LogWarning(msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasAssetOrDependencyChanged(CachedInfo info)
|
||||
{
|
||||
if (info == null || !info.Asset.IsValid())
|
||||
return true;
|
||||
|
||||
var result = false;
|
||||
var updatedEntry = GetUpdatedCacheEntry(info.Asset);
|
||||
if (info.Asset != updatedEntry)
|
||||
{
|
||||
if (!LogCacheMiss($"[Cache Miss]: Source asset changed. Old: {info.Asset} New: {updatedEntry}"))
|
||||
return true;
|
||||
result = true;
|
||||
}
|
||||
|
||||
foreach (var dependency in info.Dependencies)
|
||||
{
|
||||
if (!dependency.IsValid())
|
||||
{
|
||||
if (!LogCacheMiss($"[Cache Miss]: Dependency is no longer valid. Asset: {info.Asset} Dependency: {dependency}"))
|
||||
return true;
|
||||
result = true;
|
||||
}
|
||||
|
||||
updatedEntry = GetUpdatedCacheEntry(dependency);
|
||||
if (dependency != GetUpdatedCacheEntry(updatedEntry))
|
||||
{
|
||||
if (!LogCacheMiss($"[Cache Miss]: Dependency changed. Asset: {info.Asset} Old: {dependency} New: {updatedEntry}"))
|
||||
return true;
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetCachedInfoFile(CacheEntry entry)
|
||||
{
|
||||
var guid = entry.Guid.ToString();
|
||||
string finalHash = HashingMethods.Calculate(m_GlobalHash, entry.Hash).ToString();
|
||||
return string.Format("{0}/{1}/{2}/{3}/{2}.info", k_CachePath, guid.Substring(0, 2), guid, finalHash);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetCachedArtifactsDirectory(CacheEntry entry)
|
||||
{
|
||||
var guid = entry.Guid.ToString();
|
||||
string finalHash = HashingMethods.Calculate(m_GlobalHash, entry.Hash).ToString();
|
||||
return string.Format("{0}/{1}/{2}/{3}", k_CachePath, guid.Substring(0, 2), guid, finalHash);
|
||||
}
|
||||
|
||||
class FileOperations
|
||||
{
|
||||
public FileOperations(int size)
|
||||
{
|
||||
data = new FileOperation[size];
|
||||
waitLock = new Semaphore(0, size);
|
||||
}
|
||||
|
||||
public FileOperation[] data;
|
||||
public Semaphore waitLock;
|
||||
}
|
||||
|
||||
struct FileOperation
|
||||
{
|
||||
public string file;
|
||||
public MemoryStream bytes;
|
||||
}
|
||||
|
||||
static void Read(object data)
|
||||
{
|
||||
var ops = (FileOperations)data;
|
||||
for (int index = 0; index < ops.data.Length; index++, ops.waitLock.Release())
|
||||
{
|
||||
try
|
||||
{
|
||||
var op = ops.data[index];
|
||||
if (File.Exists(op.file))
|
||||
{
|
||||
byte[] bytes = File.ReadAllBytes(op.file);
|
||||
if (bytes.Length > 0)
|
||||
op.bytes = new MemoryStream(bytes, false);
|
||||
}
|
||||
ops.data[index] = op;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BuildLogger.LogException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if UNITY_2019_4_OR_NEWER
|
||||
// Newer Parallel.For concurrent method for 2019.4 and newer. ~3x faster than the old two-thread read/deserialize method when using four threads
|
||||
public void LoadCachedData(IList<CacheEntry> entries, out IList<CachedInfo> cachedInfos)
|
||||
{
|
||||
if (entries == null)
|
||||
{
|
||||
cachedInfos = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
cachedInfos = new List<CachedInfo>();
|
||||
return;
|
||||
}
|
||||
|
||||
int cachedCount = 0;
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "LoadCachedData"))
|
||||
{
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"{entries.Count} items");
|
||||
|
||||
Stopwatch deserializeTimer = null;
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "Read and deserialize cache info"))
|
||||
{
|
||||
CachedInfo[] cachedInfoArray = new CachedInfo[entries.Count];
|
||||
deserializeTimer = Stopwatch.StartNew();
|
||||
int workerThreadCount = Math.Min(Environment.ProcessorCount, 4); // Testing of the USerialize code has shown increasing concurrency beyond four threads produces worse performance (the suspicion is due to GC contention but that's TBC)
|
||||
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = workerThreadCount };
|
||||
ConcurrentStack<DeSerializer> deserializers = new ConcurrentStack<DeSerializer>();
|
||||
for (int serializerNum = 0; serializerNum < workerThreadCount; serializerNum++)
|
||||
deserializers.Push(new DeSerializer(CustomSerializers, ObjectFactories));
|
||||
Parallel.For(0, entries.Count, parallelOptions, index =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string file = GetCachedInfoFile(entries[index]);
|
||||
byte[] bytes = File.ReadAllBytes(file);
|
||||
if ((bytes != null) && (bytes.Length > 0))
|
||||
{
|
||||
using (MemoryStream memoryStream = new MemoryStream(bytes, false))
|
||||
{
|
||||
deserializers.TryPop(out DeSerializer deserializer);
|
||||
cachedInfoArray[index] = deserializer.DeSerialize<CachedInfo>(memoryStream);
|
||||
deserializers.Push(deserializer);
|
||||
Interlocked.Increment(ref cachedCount);
|
||||
}
|
||||
}
|
||||
else
|
||||
LogCacheMiss($"[Cache Miss]: Missing cache entry. Entry: {entries[index]}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
LogCacheMiss($"[Cache Miss]: Invalid cache entry. Entry: {entries[index]}");
|
||||
}
|
||||
});
|
||||
deserializeTimer.Stop();
|
||||
cachedInfos = cachedInfoArray.ToList();
|
||||
}
|
||||
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"Time spent deserializing: {deserializeTimer.ElapsedMilliseconds}ms");
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"Local Cache hit count: {cachedCount}");
|
||||
}
|
||||
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "Check for changed dependencies"))
|
||||
{
|
||||
int unchangedCount = 0;
|
||||
for (int i = 0; i < cachedInfos.Count; i++)
|
||||
{
|
||||
if (HasAssetOrDependencyChanged(cachedInfos[i]))
|
||||
cachedInfos[i] = null;
|
||||
else
|
||||
unchangedCount++;
|
||||
}
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"Unchanged dependencies count: {unchangedCount}");
|
||||
}
|
||||
|
||||
// If we have a cache server connection, download & check any missing info
|
||||
int downloadedCount = 0;
|
||||
if (m_Downloader != null)
|
||||
{
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "Download Missing Entries"))
|
||||
{
|
||||
m_Downloader.DownloadMissing(entries, cachedInfos);
|
||||
downloadedCount = cachedInfos.Count(i => i != null) - cachedCount;
|
||||
}
|
||||
}
|
||||
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"Local Cache hit count: {cachedCount}, Cache Server hit count: {downloadedCount}");
|
||||
|
||||
Assert.AreEqual(entries.Count, cachedInfos.Count);
|
||||
}
|
||||
|
||||
#else // !UNITY_2019_4_OR_NEWER
|
||||
// Old two-thread serialize/read method for 2018.4 support.
|
||||
// 2018.4 does not support us running serialization on threads other than the main thread due to functions being called in Unity that are not marked as thread safe in that version (GUIDToHexInternal() and SerializeToBinary() at least)
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LoadCachedData(IList<CacheEntry> entries, out IList<CachedInfo> cachedInfos)
|
||||
{
|
||||
if (entries == null)
|
||||
{
|
||||
cachedInfos = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
cachedInfos = new List<CachedInfo>();
|
||||
return;
|
||||
}
|
||||
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "LoadCachedData"))
|
||||
{
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"{entries.Count} items");
|
||||
// Setup Operations
|
||||
var ops = new FileOperations(entries.Count);
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "GetCachedInfoFile"))
|
||||
{
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var op = ops.data[i];
|
||||
op.file = GetCachedInfoFile(entries[i]);
|
||||
ops.data[i] = op;
|
||||
}
|
||||
}
|
||||
|
||||
int cachedCount = 0;
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "Read and deserialize cache info"))
|
||||
{
|
||||
// Start file reading
|
||||
Thread thread = new Thread(Read);
|
||||
thread.Start(ops);
|
||||
|
||||
cachedInfos = new List<CachedInfo>(entries.Count);
|
||||
|
||||
// Deserialize as files finish reading
|
||||
Stopwatch deserializeTimer = Stopwatch.StartNew();
|
||||
DeSerializer deserializer = new DeSerializer(CustomSerializers, ObjectFactories);
|
||||
for (int index = 0; index < entries.Count; index++)
|
||||
{
|
||||
// Basic wait lock
|
||||
if (!ops.waitLock.WaitOne(0))
|
||||
{
|
||||
deserializeTimer.Stop();
|
||||
ops.waitLock.WaitOne();
|
||||
deserializeTimer.Start();
|
||||
}
|
||||
|
||||
CachedInfo info = null;
|
||||
try
|
||||
{
|
||||
var op = ops.data[index];
|
||||
if (op.bytes != null && op.bytes.Length > 0)
|
||||
{
|
||||
info = deserializer.DeSerialize<CachedInfo>(op.bytes);
|
||||
cachedCount++;
|
||||
}
|
||||
else
|
||||
LogCacheMiss($"[Cache Miss]: Missing cache entry. Entry: {entries[index]}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
LogCacheMiss($"[Cache Miss]: Invalid cache entry. Entry: {entries[index]}");
|
||||
}
|
||||
cachedInfos.Add(info);
|
||||
}
|
||||
thread.Join();
|
||||
((IDisposable)ops.waitLock).Dispose();
|
||||
|
||||
deserializeTimer.Stop();
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"Time spent deserializing: {deserializeTimer.ElapsedMilliseconds}ms");
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"Local Cache hit count: {cachedCount}");
|
||||
}
|
||||
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "Check for changed dependencies"))
|
||||
{
|
||||
for (int i = 0; i < cachedInfos.Count; i++)
|
||||
{
|
||||
if (HasAssetOrDependencyChanged(cachedInfos[i]))
|
||||
cachedInfos[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a cache server connection, download & check any missing info
|
||||
int downloadedCount = 0;
|
||||
if (m_Downloader != null)
|
||||
{
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "Download Missing Entries"))
|
||||
{
|
||||
m_Downloader.DownloadMissing(entries, cachedInfos);
|
||||
downloadedCount = cachedInfos.Count(i => i != null) - cachedCount;
|
||||
}
|
||||
}
|
||||
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"Local Cache hit count: {cachedCount}, Cache Server hit count: {downloadedCount}");
|
||||
|
||||
Assert.AreEqual(entries.Count, cachedInfos.Count);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if UNITY_2019_4_OR_NEWER
|
||||
// Newer Parallel.For concurrent method for 2019.4 and newer. ~3x faster than the old two-thread serialize/write method when using four threads
|
||||
|
||||
class SaveCachedDataTaskData
|
||||
{
|
||||
public Semaphore m_ReadyLock = new Semaphore(0, 1);
|
||||
public Semaphore m_DoneLock = new Semaphore(0, 1);
|
||||
}
|
||||
|
||||
static void SaveCachedDataTask(object data)
|
||||
{
|
||||
SaveCachedDataTaskData saveTaskData = (SaveCachedDataTaskData)data;
|
||||
|
||||
// Tell the SaveCachedData() function that our task has started, ThreadingManager ensures this can only happen after any queued prune tasks have completed
|
||||
saveTaskData.m_ReadyLock.Release();
|
||||
|
||||
// Now wait until SaveCachedData() has finished serialising and writing files. The presence of this task in the Save task queue will prevent any new prune tasks being queued
|
||||
saveTaskData.m_DoneLock.WaitOne();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveCachedData(IList<CachedInfo> infos)
|
||||
{
|
||||
if (infos == null || infos.Count == 0)
|
||||
return;
|
||||
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "SaveCachedData"))
|
||||
{
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"Saving {infos.Count} infos");
|
||||
|
||||
// Queue the Save task and wait until it actually starts executing. This ensures any queued prune tasks finish before we start writing to the cache folder
|
||||
SaveCachedDataTaskData taskData = new SaveCachedDataTaskData();
|
||||
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.SaveQueue, SaveCachedDataTask, taskData);
|
||||
taskData.m_ReadyLock.WaitOne();
|
||||
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "SerializingCacheInfos[" + infos.Count + "]"))
|
||||
{
|
||||
int workerThreadCount = Math.Min(Environment.ProcessorCount, 4); // Testing of the USerialize code has shown increasing concurrency beyond four threads produces worse performance (the suspicion is due to GC contention but that's TBC)
|
||||
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = workerThreadCount };
|
||||
ConcurrentStack<Serializer> serializers = new ConcurrentStack<Serializer>();
|
||||
for (int serializerNum = 0; serializerNum < workerThreadCount; serializerNum++)
|
||||
serializers.Push(new Serializer(CustomSerializers));
|
||||
Parallel.For(0, infos.Count, parallelOptions, index =>
|
||||
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
serializers.TryPop(out Serializer serializer);
|
||||
serializer.Serialize(stream, infos[index], 1);
|
||||
serializers.Push(serializer);
|
||||
|
||||
if (stream.Length > 0)
|
||||
{
|
||||
// If we have a cache server connection, upload the cached data. The ThreadingManager.QueueTask() API used by CacheServerUploader.QueueUpload() is not thread safe so we lock around this
|
||||
if (m_Uploader != null)
|
||||
{
|
||||
lock (m_Uploader)
|
||||
{
|
||||
m_Uploader.QueueUpload(infos[index].Asset, GetCachedArtifactsDirectory(infos[index].Asset), new MemoryStream(stream.GetBuffer(), 0, (int)stream.Length, false));
|
||||
}
|
||||
}
|
||||
|
||||
string cachedInfoFilepath = GetCachedInfoFile(infos[index].Asset);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cachedInfoFilepath));
|
||||
|
||||
using (FileStream fileStream = new FileStream(cachedInfoFilepath, FileMode.Create))
|
||||
{
|
||||
fileStream.Write(stream.GetBuffer(), 0, (int)stream.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BuildLogger.LogException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Signal the Save task's SaveCachedDataTask() function that we are done so it can exit. This will allow prune tasks to proceed once again
|
||||
taskData.m_DoneLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
#else // !UNITY_2019_4_OR_NEWER
|
||||
// Old two-thread serialize/write method for 2018.4 support.
|
||||
// 2018.4 does not support us running serialization on threads other than the main thread due to functions being called in Unity that are not marked as thread safe in that version (GUIDToHexInternal() and SerializeToBinary() at least)
|
||||
|
||||
static void Write(object data)
|
||||
{
|
||||
var ops = (FileOperations)data;
|
||||
for (int index = 0; index < ops.data.Length; index++)
|
||||
{
|
||||
// Basic spin lock
|
||||
ops.waitLock.WaitOne();
|
||||
var op = ops.data[index];
|
||||
if (op.bytes != null && op.bytes.Length > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(op.file));
|
||||
using (FileStream fileStream = new FileStream(op.file, FileMode.Create))
|
||||
{
|
||||
fileStream.Write(op.bytes.GetBuffer(), 0, (int)op.bytes.Length);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BuildLogger.LogException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
((IDisposable)ops.waitLock).Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveCachedData(IList<CachedInfo> infos)
|
||||
{
|
||||
if (infos == null || infos.Count == 0)
|
||||
return;
|
||||
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "SaveCachedData"))
|
||||
{
|
||||
m_Logger.AddEntrySafe(LogLevel.Info, $"Saving {infos.Count} infos");
|
||||
// Setup Operations
|
||||
var ops = new FileOperations(infos.Count);
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "SetupOperations"))
|
||||
{
|
||||
for (int i = 0; i < infos.Count; i++)
|
||||
{
|
||||
var op = ops.data[i];
|
||||
op.file = GetCachedInfoFile(infos[i].Asset);
|
||||
ops.data[i] = op;
|
||||
}
|
||||
}
|
||||
|
||||
Serializer serializer = new Serializer(CustomSerializers);
|
||||
// Start writing thread
|
||||
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.SaveQueue, Write, ops);
|
||||
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "SerializingCacheInfos"))
|
||||
{
|
||||
// Serialize data as previous data is being written out
|
||||
for (int index = 0; index < infos.Count; index++, ops.waitLock.Release())
|
||||
{
|
||||
try
|
||||
{
|
||||
var op = ops.data[index];
|
||||
var stream = new MemoryStream();
|
||||
serializer.Serialize(stream, infos[index], 1);
|
||||
if (stream.Length > 0)
|
||||
{
|
||||
op.bytes = stream;
|
||||
ops.data[index] = op;
|
||||
|
||||
// If we have a cache server connection, upload the cached data
|
||||
if (m_Uploader != null)
|
||||
m_Uploader.QueueUpload(infos[index].Asset, GetCachedArtifactsDirectory(infos[index].Asset), new MemoryStream(stream.GetBuffer(), false));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BuildLogger.LogException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // #if UNITY_2019_4_OR_NEWER
|
||||
|
||||
internal void SyncPendingSaves()
|
||||
{
|
||||
using (m_Logger.ScopedStep(LogLevel.Info, "SyncPendingSaves"))
|
||||
ThreadingManager.WaitForOutstandingTasks();
|
||||
}
|
||||
|
||||
internal struct CacheFolder
|
||||
{
|
||||
public DirectoryInfo directory;
|
||||
public long Length { get; set; }
|
||||
public void Delete() => directory.Delete(true);
|
||||
public DateTime LastAccessTimeUtc
|
||||
{
|
||||
get => directory.LastAccessTimeUtc;
|
||||
internal set => directory.LastAccessTimeUtc = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the build cache directory.
|
||||
/// </summary>
|
||||
/// <param name="prompt">The message to display in the popup window.</param>
|
||||
public static void PurgeCache(bool prompt)
|
||||
{
|
||||
ThreadingManager.WaitForOutstandingTasks();
|
||||
BuildCacheUtility.ClearCacheHashes();
|
||||
if (!Directory.Exists(k_CachePath))
|
||||
{
|
||||
if (prompt)
|
||||
UnityEngine.Debug.Log("Current build cache is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (prompt)
|
||||
{
|
||||
if (!EditorUtility.DisplayDialog("Purge Build Cache", "Do you really want to purge your entire build cache?", "Yes", "No"))
|
||||
return;
|
||||
|
||||
EditorUtility.DisplayProgressBar(ScriptableBuildPipeline.Properties.purgeCache.text, ScriptableBuildPipeline.Properties.pleaseWait.text, 0.0F);
|
||||
Directory.Delete(k_CachePath, true);
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
else
|
||||
Directory.Delete(k_CachePath, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prunes the build cache so that its size is within the maximum cache size.
|
||||
/// </summary>
|
||||
public static void PruneCache()
|
||||
{
|
||||
ThreadingManager.WaitForOutstandingTasks();
|
||||
int maximumSize = ScriptableBuildPipeline.maximumCacheSize;
|
||||
long maximumCacheSize = maximumSize * k_BytesToGigaBytes;
|
||||
|
||||
// Get sizes based on common directory root for a guid / hash
|
||||
ComputeCacheSizeAndFolders(out long currentCacheSize, out List<CacheFolder> cacheFolders);
|
||||
|
||||
if (currentCacheSize < maximumCacheSize)
|
||||
{
|
||||
UnityEngine.Debug.LogFormat("Current build cache currentCacheSize {0}, prune threshold {1} GB. No prune performed. You can change this value in the \"Edit/Preferences...\" window.", EditorUtility.FormatBytes(currentCacheSize), maximumSize);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EditorUtility.DisplayDialog("Prune Build Cache", string.Format("Current build cache currentCacheSize is {0}, which is over the prune threshold of {1}. Do you want to prune your build cache now?", EditorUtility.FormatBytes(currentCacheSize), EditorUtility.FormatBytes(maximumCacheSize)), "Yes", "No"))
|
||||
return;
|
||||
|
||||
EditorUtility.DisplayProgressBar(ScriptableBuildPipeline.Properties.pruneCache.text, ScriptableBuildPipeline.Properties.pleaseWait.text, 0.0F);
|
||||
|
||||
PruneCacheFolders(maximumCacheSize, currentCacheSize, cacheFolders);
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prunes the build cache without showing UI prompts.
|
||||
/// </summary>
|
||||
/// <param name="maximumCacheSize">The maximum cache size.</param>
|
||||
public static void PruneCache_Background(long maximumCacheSize)
|
||||
{
|
||||
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.PruneQueue, PruneCache_Background_Internal, maximumCacheSize);
|
||||
}
|
||||
|
||||
internal static void PruneCache_Background_Internal(object maximumCacheSize)
|
||||
{
|
||||
long maxCacheSize = (long)maximumCacheSize;
|
||||
// Get sizes based on common directory root for a guid / hash
|
||||
ComputeCacheSizeAndFolders(out long currentCacheSize, out List<CacheFolder> cacheFolders);
|
||||
if (currentCacheSize < maxCacheSize)
|
||||
return;
|
||||
|
||||
PruneCacheFolders(maxCacheSize, currentCacheSize, cacheFolders);
|
||||
}
|
||||
|
||||
internal static void ComputeCacheSizeAndFolders(out long currentCacheSize, out List<CacheFolder> cacheFolders)
|
||||
{
|
||||
currentCacheSize = 0;
|
||||
cacheFolders = new List<CacheFolder>();
|
||||
|
||||
var directory = new DirectoryInfo(k_CachePath);
|
||||
if (!directory.Exists)
|
||||
return;
|
||||
|
||||
int length = directory.FullName.Count(x => x == Path.DirectorySeparatorChar) + 3;
|
||||
DirectoryInfo[] subDirectories = directory.GetDirectories("*", SearchOption.AllDirectories);
|
||||
foreach (var subDirectory in subDirectories)
|
||||
{
|
||||
if (subDirectory.FullName.Count(x => x == Path.DirectorySeparatorChar) != length)
|
||||
continue;
|
||||
|
||||
FileInfo[] files = subDirectory.GetFiles("*", SearchOption.AllDirectories);
|
||||
var cacheFolder = new CacheFolder { directory = subDirectory, Length = files.Sum(x => x.Length) };
|
||||
cacheFolders.Add(cacheFolder);
|
||||
|
||||
currentCacheSize += cacheFolder.Length;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void PruneCacheFolders(long maximumCacheSize, long currentCacheSize, List<CacheFolder> cacheFolders)
|
||||
{
|
||||
cacheFolders.Sort((a, b) => a.LastAccessTimeUtc.CompareTo(b.LastAccessTimeUtc));
|
||||
// Need to delete sets of files as the .info might reference a specific file artifact
|
||||
foreach (var cacheFolder in cacheFolders)
|
||||
{
|
||||
currentCacheSize -= cacheFolder.Length;
|
||||
cacheFolder.Delete();
|
||||
if (currentCacheSize < maximumCacheSize)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add to IBuildCache interface when IBuildLogger becomes public
|
||||
internal void SetBuildLogger(IBuildLogger profiler)
|
||||
{
|
||||
m_Logger = profiler;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e2bbbc81002ad3642a2b1e7857a3f11a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,200 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build.Content;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
using UnityEditor.Build.Pipeline.Utilities;
|
||||
using UnityEditor.Build.Player;
|
||||
using UnityEditor.Build.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
internal class AutoBuildCacheUtility : IDisposable
|
||||
{
|
||||
public AutoBuildCacheUtility()
|
||||
{
|
||||
BuildCacheUtility.ClearCacheHashes();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BuildCacheUtility.ClearCacheHashes();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class BuildCacheUtility
|
||||
{
|
||||
internal static Dictionary<KeyValuePair<GUID, int>, CacheEntry> m_GuidToHash = new Dictionary<KeyValuePair<GUID, int>, CacheEntry>();
|
||||
static Dictionary<KeyValuePair<string, int>, CacheEntry> m_PathToHash = new Dictionary<KeyValuePair<string, int>, CacheEntry>();
|
||||
static Dictionary<KeyValuePair<Type, int>, CacheEntry> m_TypeToHash = new Dictionary<KeyValuePair<Type, int>, CacheEntry>();
|
||||
static Dictionary<ObjectIdentifier, Type[]> m_ObjectToType = new Dictionary<ObjectIdentifier, Type[]>();
|
||||
static TypeDB m_TypeDB;
|
||||
internal static HashSet<GUID> m_ExplicitAssets = new HashSet<GUID>();
|
||||
|
||||
#if !ENABLE_TYPE_HASHING
|
||||
static Hash128 m_UnityVersion = HashingMethods.Calculate(Application.unityVersion).ToHash128();
|
||||
#endif
|
||||
|
||||
public static void SetCurrentBuildContent(IBuildContent content)
|
||||
{
|
||||
m_ExplicitAssets.Clear();
|
||||
foreach (GUID asset in content.Assets)
|
||||
m_ExplicitAssets.Add(asset);
|
||||
foreach (GUID scene in content.Scenes)
|
||||
m_ExplicitAssets.Add(scene);
|
||||
}
|
||||
|
||||
public static CacheEntry GetCacheEntry(GUID asset, int version = 1)
|
||||
{
|
||||
CacheEntry entry;
|
||||
KeyValuePair<GUID, int> key = new KeyValuePair<GUID, int>(asset, version);
|
||||
if (m_GuidToHash.TryGetValue(key, out entry))
|
||||
return entry;
|
||||
|
||||
entry = new CacheEntry { Guid = asset, Version = version };
|
||||
string path = AssetDatabase.GUIDToAssetPath(asset.ToString());
|
||||
entry.Type = CacheEntry.EntryType.Asset;
|
||||
|
||||
if (path.Equals(CommonStrings.UnityBuiltInExtraPath, StringComparison.OrdinalIgnoreCase) || path.Equals(CommonStrings.UnityDefaultResourcePath, StringComparison.OrdinalIgnoreCase))
|
||||
entry.Hash = HashingMethods.Calculate(Application.unityVersion, path).ToHash128();
|
||||
else
|
||||
{
|
||||
entry.Hash = AssetDatabase.GetAssetDependencyHash(path);
|
||||
if (!entry.Hash.isValid && File.Exists(path))
|
||||
entry.Hash = HashingMethods.CalculateFile(path).ToHash128();
|
||||
if (path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
|
||||
entry.Hash = HashingMethods.Calculate(entry.Hash, BuildInterfacesWrapper.SceneCallbackVersionHash, PlayerSettings.stripUnusedMeshComponents).ToHash128();
|
||||
}
|
||||
|
||||
if (entry.Hash.isValid)
|
||||
entry.Hash = HashingMethods.Calculate(entry.Hash, entry.Version).ToHash128();
|
||||
|
||||
entry.Inclusion = m_ExplicitAssets.Contains(asset) ? CacheEntry.InclusionType.Explicit : CacheEntry.InclusionType.Implicit;
|
||||
|
||||
m_GuidToHash[key] = entry;
|
||||
return entry;
|
||||
}
|
||||
|
||||
public static CacheEntry GetCacheEntry(string path, int version = 1)
|
||||
{
|
||||
CacheEntry entry;
|
||||
KeyValuePair<string, int> key = new KeyValuePair<string, int>(path, version);
|
||||
if (m_PathToHash.TryGetValue(key, out entry))
|
||||
return entry;
|
||||
|
||||
var guid = AssetDatabase.AssetPathToGUID(path);
|
||||
if (!string.IsNullOrEmpty(guid))
|
||||
return GetCacheEntry(new GUID(guid), version);
|
||||
|
||||
entry = new CacheEntry { File = path, Version = version };
|
||||
entry.Guid = HashingMethods.Calculate("FileHash", entry.File).ToGUID();
|
||||
if (File.Exists(entry.File))
|
||||
entry.Hash = HashingMethods.Calculate(HashingMethods.CalculateFile(entry.File), entry.Version).ToHash128();
|
||||
entry.Type = CacheEntry.EntryType.File;
|
||||
|
||||
m_PathToHash[key] = entry;
|
||||
return entry;
|
||||
}
|
||||
|
||||
public static CacheEntry GetCacheEntry(Type type, int version = 1)
|
||||
{
|
||||
CacheEntry entry;
|
||||
KeyValuePair<Type, int> key = new KeyValuePair<Type, int>(type, version);
|
||||
if (m_TypeToHash.TryGetValue(key, out entry))
|
||||
return entry;
|
||||
|
||||
entry = new CacheEntry { ScriptType = type.AssemblyQualifiedName, Version = version };
|
||||
entry.Guid = HashingMethods.Calculate("TypeHash", entry.ScriptType).ToGUID();
|
||||
#if ENABLE_TYPE_HASHING
|
||||
entry.Hash = ContentBuildInterface.CalculatePlayerSerializationHashForType(type, m_TypeDB);
|
||||
#else
|
||||
entry.Hash = m_TypeDB != null ? m_TypeDB.GetHash128() : m_UnityVersion;
|
||||
#endif
|
||||
entry.Type = CacheEntry.EntryType.ScriptType;
|
||||
|
||||
m_TypeToHash[key] = entry;
|
||||
return entry;
|
||||
}
|
||||
|
||||
static Type[] GetCachedTypesForObject(ObjectIdentifier objectId)
|
||||
{
|
||||
if (!m_ObjectToType.TryGetValue(objectId, out Type[] types))
|
||||
{
|
||||
#if ENABLE_TYPE_HASHING
|
||||
types = ContentBuildInterface.GetTypesForObject(objectId);
|
||||
#else
|
||||
types = ContentBuildInterface.GetTypeForObjects(new[] { objectId });
|
||||
#endif
|
||||
m_ObjectToType[objectId] = types;
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
public static Type GetMainTypeForObject(ObjectIdentifier objectId)
|
||||
{
|
||||
Type[] types = GetCachedTypesForObject(objectId);
|
||||
return types[0];
|
||||
}
|
||||
|
||||
public static Type[] GetMainTypeForObjects(IEnumerable<ObjectIdentifier> objectIds)
|
||||
{
|
||||
List<Type> results = new List<Type>();
|
||||
foreach (var objectId in objectIds)
|
||||
{
|
||||
Type[] types = GetCachedTypesForObject(objectId);
|
||||
results.Add(types[0]);
|
||||
}
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public static Type[] GetSortedUniqueTypesForObject(ObjectIdentifier objectId)
|
||||
{
|
||||
Type[] types = GetCachedTypesForObject(objectId);
|
||||
Array.Sort(types, (x, y) => x.AssemblyQualifiedName.CompareTo(y.AssemblyQualifiedName));
|
||||
return types;
|
||||
}
|
||||
|
||||
public static Type[] GetSortedUniqueTypesForObjects(IEnumerable<ObjectIdentifier> objectIds)
|
||||
{
|
||||
Type[] types;
|
||||
HashSet<Type> results = new HashSet<Type>();
|
||||
foreach (var objectId in objectIds)
|
||||
{
|
||||
types = GetCachedTypesForObject(objectId);
|
||||
results.UnionWith(types);
|
||||
}
|
||||
types = results.ToArray();
|
||||
Array.Sort(types, (x, y) => x.AssemblyQualifiedName.CompareTo(y.AssemblyQualifiedName));
|
||||
return types;
|
||||
}
|
||||
|
||||
public static void SetTypeForObjects(IEnumerable<ObjectTypes> pairs)
|
||||
{
|
||||
foreach (var pair in pairs)
|
||||
m_ObjectToType[pair.ObjectID] = pair.Types;
|
||||
}
|
||||
|
||||
internal static void ClearCacheHashes()
|
||||
{
|
||||
m_GuidToHash.Clear();
|
||||
m_PathToHash.Clear();
|
||||
m_TypeToHash.Clear();
|
||||
m_ObjectToType.Clear();
|
||||
m_TypeDB = null;
|
||||
}
|
||||
|
||||
public static void SetTypeDB(TypeDB typeDB)
|
||||
{
|
||||
if (m_TypeToHash.Count > 0)
|
||||
throw new InvalidOperationException("Changing Player TypeDB mid build is not supported at this time.");
|
||||
m_TypeDB = typeDB;
|
||||
}
|
||||
|
||||
public static CacheEntry GetCacheEntry(ObjectIdentifier objectID, int version = 1)
|
||||
{
|
||||
if (objectID.guid.Empty())
|
||||
return GetCacheEntry(objectID.filePath, version);
|
||||
return GetCacheEntry(objectID.guid, version);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b4b0a7d96d7360c4abef5bae019289c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,130 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal interface so switch platform build task can initialize editor build callbacks
|
||||
/// </summary>
|
||||
internal interface IEditorBuildCallbacks : IContextObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Callbacks need to be Initialized after platform switch
|
||||
/// </summary>
|
||||
void InitializeCallbacks();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages initialization and cleanup of Unity Editor IPreprocessShaders, IProcessScene, & IProcessSceneWithReport build callbacks.
|
||||
/// </summary>
|
||||
public class BuildInterfacesWrapper : IDisposable, IEditorBuildCallbacks
|
||||
{
|
||||
Type m_Type = null;
|
||||
bool m_Disposed = false;
|
||||
|
||||
internal static Hash128 SceneCallbackVersionHash = new Hash128();
|
||||
internal static Hash128 ShaderCallbackVersionHash = new Hash128();
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor, initializes properties to defaults
|
||||
/// </summary>
|
||||
public BuildInterfacesWrapper()
|
||||
{
|
||||
m_Type = Type.GetType("UnityEditor.Build.BuildPipelineInterfaces, UnityEditor");
|
||||
InitializeCallbacks();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public dispose function when instance is not in a using statement and manual dispose is required
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the build interfaces wrapper instance.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Obsolete parameter.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (m_Disposed)
|
||||
return;
|
||||
|
||||
CleanupCallbacks();
|
||||
m_Disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes Unity Editor IPreprocessShaders, IPreprocessComputeShaders, IProcessScene, & IProcessSceneWithReport build callbacks.
|
||||
/// </summary>
|
||||
public void InitializeCallbacks()
|
||||
{
|
||||
var init = m_Type.GetMethod("InitializeBuildCallbacks", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
init.Invoke(null, new object[] { 274 }); // 274 = BuildCallbacks.SceneProcessors | BuildCallbacks.ShaderProcessors | BuildCallbacks.ComputeShader
|
||||
#else
|
||||
init.Invoke(null, new object[] { 18 }); // 18 = BuildCallbacks.SceneProcessors | BuildCallbacks.ShaderProcessors
|
||||
#endif
|
||||
|
||||
#if UNITY_2019_4_OR_NEWER
|
||||
GatherCallbackVersions();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_2019_4_OR_NEWER
|
||||
internal void GatherCallbackVersions()
|
||||
{
|
||||
var versionedType = typeof(VersionedCallbackAttribute);
|
||||
var typeCollection = TypeCache.GetTypesWithAttribute(versionedType);
|
||||
List<Hash128> sceneInputs = new List<Hash128>();
|
||||
List<Hash128> shaderInputs = new List<Hash128>();
|
||||
foreach (var type in typeCollection)
|
||||
{
|
||||
var attribute = (VersionedCallbackAttribute)Attribute.GetCustomAttribute(type, versionedType);
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
if (typeof(IPreprocessShaders).IsAssignableFrom(type) || typeof(IPreprocessComputeShaders).IsAssignableFrom(type))
|
||||
#else
|
||||
if (typeof(IPreprocessShaders).IsAssignableFrom(type))
|
||||
#endif
|
||||
{
|
||||
shaderInputs.Add(HashingMethods.Calculate(type.AssemblyQualifiedName, attribute.version).ToHash128());
|
||||
}
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
else if (typeof(IProcessScene).IsAssignableFrom(type) || typeof(IProcessSceneWithReport).IsAssignableFrom(type))
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
sceneInputs.Add(HashingMethods.Calculate(type.AssemblyQualifiedName, attribute.version).ToHash128());
|
||||
}
|
||||
}
|
||||
|
||||
SceneCallbackVersionHash = new Hash128();
|
||||
if (sceneInputs.Count > 0)
|
||||
{
|
||||
sceneInputs.Sort();
|
||||
SceneCallbackVersionHash = HashingMethods.Calculate(sceneInputs).ToHash128();
|
||||
}
|
||||
|
||||
ShaderCallbackVersionHash = new Hash128();
|
||||
if (shaderInputs.Count > 0)
|
||||
{
|
||||
shaderInputs.Sort();
|
||||
ShaderCallbackVersionHash = HashingMethods.Calculate(shaderInputs).ToHash128();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup Unity Editor IPreprocessShaders, IProcessScene, & IProcessSceneWithReport build callbacks.
|
||||
/// </summary>
|
||||
public void CleanupCallbacks()
|
||||
{
|
||||
var clean = m_Type.GetMethod("CleanupBuildCallbacks", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
clean.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cff0546d9c35a3248bd820d7dd136af6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,349 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Basic implementation of IBuildLogger. Stores events in memory and can dump them to the trace event format.
|
||||
/// <seealso cref="IBuildLogger"/>
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class BuildLog : IBuildLogger, IDeferredBuildLogger
|
||||
{
|
||||
[Serializable]
|
||||
internal struct LogEntry
|
||||
{
|
||||
public int ThreadId { get; set; }
|
||||
public double Time { get; set; }
|
||||
public LogLevel Level { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
internal class LogStep
|
||||
{
|
||||
List<LogStep> m_Children;
|
||||
List<LogEntry> m_Entries;
|
||||
|
||||
public string Name { get; set; }
|
||||
public LogLevel Level { get; set; }
|
||||
public List<LogStep> Children { get { if (m_Children == null) m_Children = new List<LogStep>(); return m_Children; } }
|
||||
public List<LogEntry> Entries { get { if (m_Entries == null) m_Entries = new List<LogEntry>(); return m_Entries; } }
|
||||
public double DurationMS { get; private set; }
|
||||
public int ThreadId { get; set; }
|
||||
public double StartTime { get; set; }
|
||||
internal bool isThreaded;
|
||||
|
||||
public bool HasChildren { get { return Children != null && Children.Count > 0; } }
|
||||
public bool HasEntries { get { return Entries != null && Entries.Count > 0; } }
|
||||
|
||||
internal void Complete(double time)
|
||||
{
|
||||
DurationMS = time - StartTime;
|
||||
}
|
||||
}
|
||||
|
||||
LogStep m_Root;
|
||||
[NonSerialized]
|
||||
Stack<LogStep> m_Stack;
|
||||
[NonSerialized]
|
||||
ThreadLocal<BuildLog> m_ThreadedLogs;
|
||||
[NonSerialized]
|
||||
Stopwatch m_WallTimer;
|
||||
|
||||
bool m_ShouldOverrideWallTimer;
|
||||
double m_WallTimerOverride;
|
||||
|
||||
double GetWallTime()
|
||||
{
|
||||
return m_ShouldOverrideWallTimer ? m_WallTimerOverride : m_WallTimer.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
||||
void Init(bool onThread)
|
||||
{
|
||||
m_WallTimer = Stopwatch.StartNew();
|
||||
m_Root = new LogStep();
|
||||
m_Stack = new Stack<LogStep>();
|
||||
m_Stack.Push(m_Root);
|
||||
|
||||
AddMetaData("Date", DateTime.Now.ToString());
|
||||
|
||||
if (!onThread)
|
||||
{
|
||||
AddMetaData("UnityVersion", UnityEngine.Application.unityVersion);
|
||||
#if UNITY_2019_2_OR_NEWER // PackageManager package inspection APIs didn't exist until 2019.2
|
||||
PackageManager.PackageInfo info = PackageManager.PackageInfo.FindForAssembly(typeof(BuildLog).Assembly);
|
||||
if (info != null)
|
||||
AddMetaData(info.name, info.version);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new build log object.
|
||||
/// </summary>
|
||||
public BuildLog()
|
||||
{
|
||||
Init(false);
|
||||
}
|
||||
|
||||
internal BuildLog(bool onThread)
|
||||
{
|
||||
Init(onThread);
|
||||
}
|
||||
|
||||
private BuildLog GetThreadSafeLog()
|
||||
{
|
||||
if (m_ThreadedLogs != null)
|
||||
{
|
||||
if (!m_ThreadedLogs.IsValueCreated)
|
||||
m_ThreadedLogs.Value = new BuildLog(true);
|
||||
return m_ThreadedLogs.Value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void BeginBuildStep(LogLevel level, string stepName, bool multiThreaded)
|
||||
{
|
||||
BuildLog log = GetThreadSafeLog();
|
||||
BeginBuildStepInternal(log, level, stepName, multiThreaded);
|
||||
}
|
||||
|
||||
private static void BeginBuildStepInternal(BuildLog log, LogLevel level, string stepName, bool multiThreaded)
|
||||
{
|
||||
LogStep node = new LogStep();
|
||||
node.Level = level;
|
||||
node.Name = stepName;
|
||||
node.StartTime = log.GetWallTime();
|
||||
node.ThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
log.m_Stack.Peek().Children.Add(node);
|
||||
log.m_Stack.Push(node);
|
||||
if (multiThreaded)
|
||||
{
|
||||
Debug.Assert(log.m_ThreadedLogs == null);
|
||||
log.m_ThreadedLogs = new ThreadLocal<BuildLog>(true);
|
||||
log.m_ThreadedLogs.Value = log;
|
||||
node.isThreaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void EndBuildStep()
|
||||
{
|
||||
EndBuildStepInternal(GetThreadSafeLog());
|
||||
}
|
||||
|
||||
private static void OffsetTimesR(LogStep step, double offset)
|
||||
{
|
||||
step.StartTime += offset;
|
||||
if (step.HasEntries)
|
||||
{
|
||||
for (int i = 0; i < step.Entries.Count; i++)
|
||||
{
|
||||
LogEntry e = step.Entries[i];
|
||||
e.Time = e.Time + offset;
|
||||
step.Entries[i] = e;
|
||||
}
|
||||
}
|
||||
if (step.HasChildren)
|
||||
foreach (LogStep subStep in step.Children)
|
||||
OffsetTimesR(subStep, offset);
|
||||
}
|
||||
|
||||
private static void EndBuildStepInternal(BuildLog log)
|
||||
{
|
||||
Debug.Assert(log.m_Stack.Count > 1);
|
||||
LogStep node = log.m_Stack.Pop();
|
||||
node.Complete(log.GetWallTime());
|
||||
|
||||
if (node.isThreaded)
|
||||
{
|
||||
foreach (BuildLog subLog in log.m_ThreadedLogs.Values)
|
||||
{
|
||||
if (subLog != log)
|
||||
{
|
||||
OffsetTimesR(subLog.Root, node.StartTime);
|
||||
if (subLog.Root.HasChildren)
|
||||
node.Children.AddRange(subLog.Root.Children);
|
||||
|
||||
if (subLog.Root.HasEntries)
|
||||
node.Entries.AddRange(subLog.Root.Entries);
|
||||
}
|
||||
}
|
||||
log.m_ThreadedLogs.Dispose();
|
||||
log.m_ThreadedLogs = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal LogStep Root { get { return m_Root; } }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddEntry(LogLevel level, string msg)
|
||||
{
|
||||
BuildLog log = GetThreadSafeLog();
|
||||
log.m_Stack.Peek().Entries.Add(new LogEntry() { Level = level, Message = msg, Time = log.GetWallTime(), ThreadId = Thread.CurrentThread.ManagedThreadId });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal use only.
|
||||
/// <seealso cref="IBuildLogger"/>
|
||||
/// </summary>
|
||||
/// <param name="events">Event collection to handle</param>
|
||||
void IDeferredBuildLogger.HandleDeferredEventStream(IEnumerable<DeferredEvent> events)
|
||||
{
|
||||
HandleDeferredEventStreamInternal(events);
|
||||
}
|
||||
|
||||
internal void HandleDeferredEventStreamInternal(IEnumerable<DeferredEvent> events)
|
||||
{
|
||||
// now make all those times relative to the active event
|
||||
LogStep startStep = m_Stack.Peek();
|
||||
|
||||
m_ShouldOverrideWallTimer = true;
|
||||
foreach (DeferredEvent e in events)
|
||||
{
|
||||
m_WallTimerOverride = e.Time + startStep.StartTime;
|
||||
if (e.Type == DeferredEventType.Begin)
|
||||
{
|
||||
BeginBuildStep(e.Level, e.Name, false);
|
||||
if (!string.IsNullOrEmpty(e.Context))
|
||||
AddEntry(e.Level, e.Context);
|
||||
}
|
||||
else if (e.Type == DeferredEventType.End)
|
||||
EndBuildStep();
|
||||
else
|
||||
AddEntry(e.Level, e.Name);
|
||||
}
|
||||
m_ShouldOverrideWallTimer = false;
|
||||
|
||||
LogStep stopStep = m_Stack.Peek();
|
||||
if (stopStep != startStep)
|
||||
throw new Exception("Deferred events did not line up as expected");
|
||||
}
|
||||
|
||||
static void AppendLineIndented(StringBuilder builder, int indentCount, string text)
|
||||
{
|
||||
for (int i = 0; i < indentCount; i++)
|
||||
builder.Append(" ");
|
||||
builder.AppendLine(text);
|
||||
}
|
||||
|
||||
static void PrintNodeR(bool includeSelf, StringBuilder builder, int indentCount, BuildLog.LogStep node)
|
||||
{
|
||||
if (includeSelf)
|
||||
AppendLineIndented(builder, indentCount, $"[{node.Name}] {node.DurationMS * 1000}us");
|
||||
foreach (var msg in node.Entries)
|
||||
{
|
||||
string line = (msg.Level == LogLevel.Warning || msg.Level == LogLevel.Error) ? $"{msg.Level}: {msg.Message}" : msg.Message;
|
||||
AppendLineIndented(builder, indentCount + 1, line);
|
||||
}
|
||||
foreach (var child in node.Children)
|
||||
PrintNodeR(true, builder, indentCount + 1, child);
|
||||
}
|
||||
|
||||
internal string FormatAsText()
|
||||
{
|
||||
using (new CultureScope())
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
PrintNodeR(false, builder, -1, Root);
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
static string CleanJSONText(string message)
|
||||
{
|
||||
return message.Replace("\\", "\\\\");
|
||||
}
|
||||
|
||||
static IEnumerable<string> IterateTEPLines(bool includeSelf, BuildLog.LogStep node)
|
||||
{
|
||||
ulong us = (ulong)(node.StartTime * 1000);
|
||||
|
||||
string argText = string.Empty;
|
||||
if (node.Entries.Count > 0)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append(", \"args\": {");
|
||||
for (int i = 0; i < node.Entries.Count; i++)
|
||||
{
|
||||
string line = (node.Entries[i].Level == LogLevel.Warning || node.Entries[i].Level == LogLevel.Error) ? $"{node.Entries[i].Level}: {node.Entries[i].Message}" : node.Entries[i].Message;
|
||||
builder.Append($"\"{i}\":\"{CleanJSONText(line)}\"");
|
||||
if (i < (node.Entries.Count - 1))
|
||||
builder.Append(", ");
|
||||
}
|
||||
builder.Append("}");
|
||||
argText = builder.ToString();
|
||||
}
|
||||
|
||||
if (includeSelf)
|
||||
yield return "{" + $"\"name\": \"{CleanJSONText(node.Name)}\", \"ph\": \"X\", \"dur\": {node.DurationMS * 1000}, \"tid\": {node.ThreadId}, \"ts\": {us}, \"pid\": 1" + argText + "}";
|
||||
|
||||
foreach (var child in node.Children)
|
||||
foreach (var r in IterateTEPLines(true, child))
|
||||
yield return r;
|
||||
}
|
||||
|
||||
class CultureScope : IDisposable
|
||||
{
|
||||
CultureInfo m_Prev;
|
||||
public CultureScope()
|
||||
{
|
||||
m_Prev = Thread.CurrentThread.CurrentCulture;
|
||||
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = m_Prev;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Tuple<string, string>> m_MetaData = new List<Tuple<string, string>>();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a key value pair to the MetaData list. This can be used to store things like package version numbers.
|
||||
/// </summary>
|
||||
/// <param name="key">The key for the MetaData.</param>
|
||||
/// <param name="value">The value of the MetaData.</param>
|
||||
public void AddMetaData(string key, string value)
|
||||
{
|
||||
m_MetaData.Add(new Tuple<string, string>(key, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the captured build log events into the text Trace Event Profiler format
|
||||
/// </summary>
|
||||
/// <returns>Profile data.</returns>
|
||||
public string FormatForTraceEventProfiler()
|
||||
{
|
||||
using (new CultureScope())
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("{");
|
||||
|
||||
foreach (Tuple<string, string> tuple in m_MetaData)
|
||||
builder.AppendLine($"\"{tuple.Item1}\": \"{tuple.Item2}\",");
|
||||
|
||||
builder.AppendLine("\"traceEvents\": [");
|
||||
int i = 0;
|
||||
foreach (string line in IterateTEPLines(false, Root))
|
||||
{
|
||||
if (i != 0)
|
||||
builder.Append(",");
|
||||
builder.AppendLine(line);
|
||||
i++;
|
||||
}
|
||||
builder.AppendLine("]");
|
||||
builder.AppendLine("}");
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: feaf7c3171943034785f9cfaa726f10d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,137 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
/// <summary>
|
||||
/// Logging overrides for SBP build logs.
|
||||
/// </summary>
|
||||
public static class BuildLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs build cache information.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message to display.</param>
|
||||
/// <param name="attrs">The objects formatted in the message.</param>
|
||||
[Conditional("BUILD_CACHE_DEBUG")]
|
||||
public static void LogCache(string msg, params object[] attrs)
|
||||
{
|
||||
Log(msg, attrs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning about the build cache.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message to display.</param>
|
||||
/// <param name="attrs">The objects formatted in the message.</param>
|
||||
[Conditional("BUILD_CACHE_DEBUG")]
|
||||
public static void LogCacheWarning(string msg, params object[] attrs)
|
||||
{
|
||||
LogWarning(msg, attrs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs general information.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message to display.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void Log(string msg)
|
||||
{
|
||||
Debug.Log(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs general information.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message object to display.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void Log(object msg)
|
||||
{
|
||||
Debug.Log(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs general information.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message to display.</param>
|
||||
/// <param name="attrs">The objects formatted in the message.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void Log(string msg, params object[] attrs)
|
||||
{
|
||||
Debug.Log(string.Format(msg, attrs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a general warning.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message to display.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void LogWarning(string msg)
|
||||
{
|
||||
Debug.LogWarning(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a general warning.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message object to display.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void LogWarning(object msg)
|
||||
{
|
||||
Debug.LogWarning(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a general warning.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message object to display.</param>
|
||||
/// <param name="attrs">The objects formatted in the message.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void LogWarning(string msg, params object[] attrs)
|
||||
{
|
||||
Debug.LogWarning(string.Format(msg, attrs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a general error.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message to display.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void LogError(string msg)
|
||||
{
|
||||
Debug.LogError(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a general error.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message object to display.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void LogError(object msg)
|
||||
{
|
||||
Debug.LogError(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a general error.
|
||||
/// </summary>
|
||||
/// <param name="msg">The message to display.</param>
|
||||
/// <param name="attrs">The objects formatted in the message.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void LogError(string msg, params object[] attrs)
|
||||
{
|
||||
Debug.LogError(string.Format(msg, attrs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a general exception.
|
||||
/// </summary>
|
||||
/// <param name="e">The exception to display.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void LogException(System.Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 91081fbba8a37084c8b7e9d7d5995080
|
||||
timeCreated: 1504709533
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,171 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
interface ICachedData {}
|
||||
|
||||
/// <summary>
|
||||
/// Stores asset information for the cache.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CachedInfo : ICachedData
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the asset.
|
||||
/// </summary>
|
||||
public CacheEntry Asset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the asset dependencies.
|
||||
/// </summary>
|
||||
public CacheEntry[] Dependencies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores extra data related to the asset.
|
||||
/// </summary>
|
||||
public object[] Data { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a container to store data in build cache.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct CacheEntry : IEquatable<CacheEntry>
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the cache entry type.
|
||||
/// </summary>
|
||||
public enum EntryType
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the entry is an asset.
|
||||
/// </summary>
|
||||
Asset,
|
||||
/// <summary>
|
||||
/// Indicates that the entry is a file.
|
||||
/// </summary>
|
||||
File,
|
||||
/// <summary>
|
||||
/// Indicates that the entry holds general data.
|
||||
/// </summary>
|
||||
Data,
|
||||
/// <summary>
|
||||
/// Indicates that the entry is a type.
|
||||
/// </summary>
|
||||
ScriptType
|
||||
}
|
||||
|
||||
internal enum InclusionType
|
||||
{
|
||||
None, Explicit, Implicit
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the entry hash.
|
||||
/// </summary>
|
||||
public Hash128 Hash { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the entry guid.
|
||||
/// </summary>
|
||||
public GUID Guid { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the entry version.
|
||||
/// </summary>
|
||||
public int Version { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the entry type.
|
||||
/// </summary>
|
||||
public EntryType Type { get; internal set; }
|
||||
|
||||
internal InclusionType Inclusion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the entry file name.
|
||||
/// </summary>
|
||||
public string File { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the entry scripting type.
|
||||
/// </summary>
|
||||
public string ScriptType { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the entry is valid.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if the entry is valid. Returns false otherwise.</returns>
|
||||
public bool IsValid()
|
||||
{
|
||||
return Hash.isValid && !Guid.Empty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the current entry instance is equal to the specified entry.
|
||||
/// </summary>
|
||||
/// <param name="obj">The entry to compare.</param>
|
||||
/// <returns>Returns true if the entries are equivalent. Returns false otherwise.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
return obj is CacheEntry && Equals((CacheEntry)obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator==(CacheEntry x, CacheEntry y)
|
||||
{
|
||||
return x.Equals(y);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static bool operator!=(CacheEntry x, CacheEntry y)
|
||||
{
|
||||
return !(x == y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash code for the cache entry.
|
||||
/// </summary>
|
||||
/// <returns>Returns the hash code for the cache entry.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hashCode = Hash.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ Guid.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ Version;
|
||||
hashCode = (hashCode * 397) ^ (int)Type;
|
||||
hashCode = (hashCode * 397) ^ (int)Inclusion;
|
||||
hashCode = (hashCode * 397) ^ (File != null ? File.GetHashCode() : 0);
|
||||
hashCode = (hashCode * 397) ^ (ScriptType != null ? ScriptType.GetHashCode() : 0);
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the information about the cache entry to a formatted string.
|
||||
/// </summary>
|
||||
/// <returns>Returns information about the cache entry.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
if (Type == EntryType.File)
|
||||
return string.Format("({0}, {1})", File, Hash);
|
||||
if (Type == EntryType.ScriptType)
|
||||
return string.Format("({0}, {1})", ScriptType, Hash);
|
||||
return string.Format("({0}, {1})", Guid, Hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the current entry instance is equal to the specified entry.
|
||||
/// </summary>
|
||||
/// <param name="other">The entry to compare.</param>
|
||||
/// <returns>Returns true if the entries are equivalent. Returns false otherwise.</returns>
|
||||
public bool Equals(CacheEntry other)
|
||||
{
|
||||
return Hash.Equals(other.Hash) && Guid.Equals(other.Guid) && Version == other.Version && Type == other.Type && Inclusion == other.Inclusion && string.Equals(File, other.File) && string.Equals(ScriptType, other.ScriptType);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4664ae20418c56944a2f6c5ab30f82bf
|
||||
timeCreated: 1504709533
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,167 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Threading;
|
||||
using UnityEditor.Build.CacheServer;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
using UnityEditor.Build.Pipeline.Utilities.USerialize;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Assertions;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
class CacheServerDownloader : IDisposable
|
||||
{
|
||||
const string k_CachePath = "Temp/CacheServer";
|
||||
|
||||
IBuildCache m_Cache;
|
||||
Client m_Client;
|
||||
Hash128 m_GlobalHash;
|
||||
DeSerializer m_Deserializer;
|
||||
|
||||
bool m_Disposed;
|
||||
|
||||
Semaphore m_Semaphore;
|
||||
|
||||
public CacheServerDownloader(IBuildCache cache, DeSerializer deserializer, string host, int port = 8126)
|
||||
{
|
||||
m_Deserializer = deserializer;
|
||||
m_Cache = cache;
|
||||
m_Client = new Client(host, port);
|
||||
m_Client.Connect();
|
||||
m_GlobalHash = new Hash128(0, 0, 0, BuildCache.k_CacheServerVersion);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!m_Disposed)
|
||||
{
|
||||
m_Disposed = true;
|
||||
m_Client.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetGlobalHash(Hash128 hash)
|
||||
{
|
||||
m_GlobalHash = hash;
|
||||
}
|
||||
|
||||
string GetCachedInfoFile(CacheEntry entry)
|
||||
{
|
||||
string finalHash = HashingMethods.Calculate(entry.Hash, m_GlobalHash).ToString();
|
||||
return string.Format("{0}/{1}_{2}.info", k_CachePath, entry.Guid.ToString(), finalHash);
|
||||
}
|
||||
|
||||
string GetCachedArtifactsFile(CacheEntry entry)
|
||||
{
|
||||
string finalHash = HashingMethods.Calculate(entry.Hash, m_GlobalHash).ToString();
|
||||
return string.Format("{0}/{1}_{2}.sbpGz", k_CachePath, entry.Guid.ToString(), finalHash);
|
||||
}
|
||||
|
||||
// Called on background thread
|
||||
void ThreadedDownloadFinished(object sender, DownloadFinishedEventArgs args)
|
||||
{
|
||||
// Only run this for Info files
|
||||
if (args.DownloadItem.Type != FileType.Info)
|
||||
return;
|
||||
|
||||
m_Semaphore.Release();
|
||||
}
|
||||
|
||||
// We don't return from this function until all downloads are processed. So it is safe to dispose immediately after.
|
||||
public void DownloadMissing(IList<CacheEntry> entries, IList<CachedInfo> cachedInfos)
|
||||
{
|
||||
Assert.AreEqual(entries.Count, cachedInfos.Count);
|
||||
Directory.CreateDirectory(k_CachePath);
|
||||
|
||||
m_Semaphore = new Semaphore(0, entries.Count);
|
||||
m_Client.DownloadFinished += ThreadedDownloadFinished;
|
||||
|
||||
// Queue up downloads for the missing or invalid local data
|
||||
for (var index = 0; index < entries.Count; index++)
|
||||
{
|
||||
// Only download data for cachedInfos that are invalid
|
||||
if (cachedInfos[index] != null)
|
||||
continue;
|
||||
|
||||
var entry = entries[index];
|
||||
|
||||
string finalHash = HashingMethods.Calculate(entry.Hash, m_GlobalHash).ToHash128().ToString();
|
||||
var fileId = FileId.From(entry.Guid.ToString(), finalHash);
|
||||
|
||||
// Download artifacts before info to ensure both are available when download for info returns
|
||||
var downloadArtifact = new FileDownloadItem(fileId, FileType.Resource, GetCachedArtifactsFile(entry));
|
||||
m_Client.QueueDownload(downloadArtifact);
|
||||
|
||||
var downloadInfo = new FileDownloadItem(fileId, FileType.Info, GetCachedInfoFile(entry));
|
||||
m_Client.QueueDownload(downloadInfo);
|
||||
}
|
||||
|
||||
// Check downloads to see if it is usable data
|
||||
var formatter = new BinaryFormatter();
|
||||
for (var index = 0; index < entries.Count; index++)
|
||||
{
|
||||
// find the next invalid cachedInfo
|
||||
while (index < entries.Count && cachedInfos[index] != null)
|
||||
index++;
|
||||
// make sure we didn't go out of bounds looking for invalid entries
|
||||
if (index >= entries.Count)
|
||||
break;
|
||||
|
||||
// Wait for info download
|
||||
m_Semaphore.WaitOne();
|
||||
|
||||
string tempInfoFile = GetCachedInfoFile(entries[index]);
|
||||
if (!File.Exists(tempInfoFile))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
CachedInfo info;
|
||||
using (var fileStream = new FileStream(tempInfoFile, FileMode.Open, FileAccess.Read))
|
||||
info = m_Deserializer.DeSerialize<CachedInfo>(fileStream);
|
||||
|
||||
if (m_Cache.HasAssetOrDependencyChanged(info))
|
||||
continue;
|
||||
|
||||
// Not every info file will have artifacts. So just check to see if we downloaded something.
|
||||
// TODO: May want to extend CachedInfo with Artifact knowledge if there is a performance benefit?
|
||||
string tempArtifactFile = GetCachedArtifactsFile(entries[index]);
|
||||
string tempArtifactDir = Path.ChangeExtension(tempArtifactFile, "");
|
||||
if (File.Exists(tempArtifactFile) && !FileCompressor.Decompress(tempArtifactFile, tempArtifactDir))
|
||||
continue;
|
||||
|
||||
// All valid, move downloaded data into place
|
||||
cachedInfos[index] = info;
|
||||
|
||||
string targetInfoFile = m_Cache.GetCachedInfoFile(info.Asset);
|
||||
if (File.Exists(targetInfoFile))
|
||||
File.Delete(targetInfoFile);
|
||||
else
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetInfoFile));
|
||||
File.Move(tempInfoFile, targetInfoFile);
|
||||
|
||||
if (Directory.Exists(tempArtifactDir))
|
||||
{
|
||||
string targetArtifactDir = m_Cache.GetCachedArtifactsDirectory(info.Asset);
|
||||
if (Directory.Exists(targetArtifactDir))
|
||||
Directory.Delete(targetArtifactDir, true);
|
||||
Directory.Move(tempArtifactDir, targetArtifactDir);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Just regenerate the artifact, logging this exception is not useful
|
||||
}
|
||||
}
|
||||
|
||||
m_Client.ResetDownloadFinishedEventHandler();
|
||||
|
||||
((IDisposable)m_Semaphore).Dispose();
|
||||
m_Semaphore = null;
|
||||
|
||||
Directory.Delete(k_CachePath, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3c6caf6db0f23fb459226c679978683d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,60 @@
|
|||
using System.IO;
|
||||
using UnityEditor.Build.CacheServer;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
class CacheServerUploader
|
||||
{
|
||||
Hash128 m_GlobalHash;
|
||||
|
||||
Client m_Client;
|
||||
|
||||
struct WorkItem
|
||||
{
|
||||
public FileId fileId;
|
||||
public string artifactsPath;
|
||||
public MemoryStream stream;
|
||||
}
|
||||
|
||||
public CacheServerUploader(string host, int port = 8126)
|
||||
{
|
||||
m_Client = new Client(host, port);
|
||||
m_Client.Connect();
|
||||
m_GlobalHash = new Hash128(0, 0, 0, BuildCache.k_CacheServerVersion);
|
||||
}
|
||||
|
||||
public void SetGlobalHash(Hash128 hash)
|
||||
{
|
||||
m_GlobalHash = hash;
|
||||
}
|
||||
|
||||
// We return from this function before all uploads are complete. So we must wait to dispose until all uploads are finished.
|
||||
public void QueueUpload(CacheEntry entry, string artifactsPath, MemoryStream stream)
|
||||
{
|
||||
var item = new WorkItem();
|
||||
string finalHash = HashingMethods.Calculate(entry.Hash, m_GlobalHash).ToString();
|
||||
item.fileId = FileId.From(entry.Guid.ToString(), finalHash);
|
||||
item.artifactsPath = artifactsPath;
|
||||
item.stream = stream;
|
||||
ThreadingManager.QueueTask(ThreadingManager.ThreadQueues.UploadQueue, UploadItem, item);
|
||||
}
|
||||
|
||||
private void UploadItem(object state)
|
||||
{
|
||||
var item = (WorkItem)state;
|
||||
m_Client.BeginTransaction(item.fileId);
|
||||
m_Client.Upload(FileType.Info, item.stream);
|
||||
|
||||
string artifactsZip = Path.GetTempFileName();
|
||||
if (FileCompressor.Compress(item.artifactsPath, artifactsZip))
|
||||
{
|
||||
using (var stream = new FileStream(artifactsZip, FileMode.Open, FileAccess.Read))
|
||||
m_Client.Upload(FileType.Resource, stream);
|
||||
}
|
||||
File.Delete(artifactsZip);
|
||||
|
||||
m_Client.EndTransaction();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 429f5bce8a9b4ba4295f2d48eebd9639
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,39 @@
|
|||
namespace UnityEditor.Build.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Static class of common strings and string formats used through out the build process
|
||||
/// </summary>
|
||||
public static class CommonStrings
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity Editor Resources path
|
||||
/// </summary>
|
||||
public const string UnityEditorResourcePath = "library/unity editor resources";
|
||||
|
||||
internal const string UnityEditorResourceGuid = "0000000000000000d000000000000000";
|
||||
|
||||
/// <summary>
|
||||
/// Unity Default Resources path
|
||||
/// </summary>
|
||||
public const string UnityDefaultResourcePath = "library/unity default resources";
|
||||
|
||||
internal const string UnityDefaultResourceGuid = "0000000000000000e000000000000000";
|
||||
|
||||
/// <summary>
|
||||
/// Unity Built-In Extras path
|
||||
/// </summary>
|
||||
public const string UnityBuiltInExtraPath = "resources/unity_builtin_extra";
|
||||
|
||||
internal const string UnityBuiltInExtraGuid = "0000000000000000f000000000000000";
|
||||
|
||||
/// <summary>
|
||||
/// Default Asset Bundle internal file name format
|
||||
/// </summary>
|
||||
public const string AssetBundleNameFormat = "archive:/{0}/{0}";
|
||||
|
||||
/// <summary>
|
||||
/// Default Scene Bundle internal file name format
|
||||
/// </summary>
|
||||
public const string SceneBundleNameFormat = "archive:/{0}/{1}.sharedAssets";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7e08d97accaeb0f47909f2371a26ca32
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,32 @@
|
|||
#if UNITY_2022_2_OR_NEWER
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build.Content;
|
||||
using UnityEditor.Build.Pipeline;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
using UnityEditor.Build.Pipeline.Utilities;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a deterministic identifier using a MD5 hash algorithm and does not require object ordering to be deterministic.
|
||||
/// This algorithm ensures objects coming from the same asset are packed closer together and can improve loading performance under certain situations.
|
||||
/// Sorts MonoScript types to the top of the file and is required when building ContentFiles.
|
||||
/// </summary>
|
||||
public class ContentFileIdentifiers : PrefabPackedIdentifiers, IDeterministicIdentifiers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override long SerializationIndexFromObjectIdentifier(ObjectIdentifier objectID)
|
||||
{
|
||||
long result = base.SerializationIndexFromObjectIdentifier(objectID);
|
||||
long mask = ((long)1 << 63);
|
||||
if (BuildCacheUtility.GetMainTypeForObject(objectID) == typeof(MonoScript))
|
||||
result |= mask;
|
||||
else
|
||||
result &= ~mask;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2b07dd9eefca94afa80d5b479a12f8cf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor.Build.Content;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
using UnityEditor.Build.Player;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
[Serializable]
|
||||
struct ObjectTypes
|
||||
{
|
||||
public ObjectIdentifier ObjectID;
|
||||
public Type[] Types;
|
||||
|
||||
public ObjectTypes(ObjectIdentifier objectID, Type[] types)
|
||||
{
|
||||
ObjectID = objectID;
|
||||
Types = types;
|
||||
}
|
||||
}
|
||||
|
||||
static class ExtensionMethods
|
||||
{
|
||||
public static bool IsNullOrEmpty<T>(this ICollection<T> collection)
|
||||
{
|
||||
return collection == null || collection.Count == 0;
|
||||
}
|
||||
|
||||
public static void GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, out TValue value) where TValue : new()
|
||||
{
|
||||
if (dictionary.TryGetValue(key, out value))
|
||||
return;
|
||||
|
||||
value = new TValue();
|
||||
dictionary.Add(key, value);
|
||||
}
|
||||
|
||||
public static void Swap<T>(this IList<T> list, int first, int second)
|
||||
{
|
||||
T temp = list[second];
|
||||
list[second] = list[first];
|
||||
list[first] = temp;
|
||||
}
|
||||
|
||||
public static void GatherSerializedObjectCacheEntries(this WriteCommand command, HashSet<CacheEntry> cacheEntries)
|
||||
{
|
||||
if (command.serializeObjects != null)
|
||||
{
|
||||
var objectIds = command.serializeObjects.Select(x => x.serializationObject);
|
||||
var types = BuildCacheUtility.GetSortedUniqueTypesForObjects(objectIds);
|
||||
cacheEntries.UnionWith(types.Select(BuildCacheUtility.GetCacheEntry));
|
||||
cacheEntries.UnionWith(objectIds.Select(BuildCacheUtility.GetCacheEntry));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ExtractCommonCacheData(IBuildCache cache, IEnumerable<ObjectIdentifier> includedObjects, IEnumerable<ObjectIdentifier> referencedObjects, HashSet<Type> uniqueTypes, List<ObjectTypes> objectTypes, HashSet<CacheEntry> dependencies)
|
||||
{
|
||||
if (includedObjects != null)
|
||||
{
|
||||
foreach (var objectId in includedObjects)
|
||||
{
|
||||
var types = BuildCacheUtility.GetSortedUniqueTypesForObject(objectId);
|
||||
objectTypes.Add(new ObjectTypes(objectId, types));
|
||||
uniqueTypes.UnionWith(types);
|
||||
}
|
||||
}
|
||||
if (referencedObjects != null)
|
||||
{
|
||||
foreach (var objectId in referencedObjects)
|
||||
{
|
||||
var types = BuildCacheUtility.GetSortedUniqueTypesForObject(objectId);
|
||||
objectTypes.Add(new ObjectTypes(objectId, types));
|
||||
uniqueTypes.UnionWith(types);
|
||||
dependencies.Add(cache.GetCacheEntry(objectId));
|
||||
}
|
||||
}
|
||||
dependencies.UnionWith(uniqueTypes.Select(cache.GetCacheEntry));
|
||||
}
|
||||
|
||||
#if NONRECURSIVE_DEPENDENCY_DATA
|
||||
public static ObjectIdentifier[] FilterReferencedObjectIDs(GUID asset, ObjectIdentifier[] references, BuildTarget target, TypeDB typeDB, HashSet<GUID> dependencies)
|
||||
{
|
||||
// Expectation: references is populated with DependencyType.ValidReferences only for the given asset
|
||||
var collectedImmediateReferences = new HashSet<ObjectIdentifier>();
|
||||
var encounteredDependencies = new HashSet<ObjectIdentifier>();
|
||||
while (references.Length > 0)
|
||||
{
|
||||
// Track which roots we encounter to do dependency pruning
|
||||
encounteredDependencies.UnionWith(references.Where(x => x.guid != asset && dependencies.Contains(x.guid)));
|
||||
// We only want to recursively grab references for objects being pulled in and won't go to another bundle
|
||||
ObjectIdentifier[] immediateReferencesNotInOtherBundles = references.Where(x => !dependencies.Contains(x.guid) && !collectedImmediateReferences.Contains(x)).ToArray();
|
||||
collectedImmediateReferences.UnionWith(immediateReferencesNotInOtherBundles);
|
||||
// Grab next set of valid references and loop
|
||||
references = ContentBuildInterface.GetPlayerDependenciesForObjects(immediateReferencesNotInOtherBundles, target, typeDB, DependencyType.ValidReferences);
|
||||
}
|
||||
|
||||
// We need to ensure that we have a reference to a visible representation so our runtime dependency appending process
|
||||
// can find something that can be appended, otherwise the necessary data will fail to load correctly in all cases. (EX: prefab A has reference to component on prefab B)
|
||||
foreach (var dependency in encounteredDependencies)
|
||||
{
|
||||
// For each dependency, add just the main representation as a reference
|
||||
var representations = ContentBuildInterface.GetPlayerAssetRepresentations(dependency.guid, target);
|
||||
collectedImmediateReferences.Add(representations.First());
|
||||
}
|
||||
collectedImmediateReferences.UnionWith(encounteredDependencies);
|
||||
return collectedImmediateReferences.ToArray();
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 10534ee7753023c4dbd1556e9efb590c
|
||||
timeCreated: 1504709533
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,132 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Since C# only has GZipStream until .NET 4.0, we are forced to implement our own packing system
|
||||
/// for artifact files. We compact all artifacts into a single GZip Stream with the header before
|
||||
/// the file name and contents. The header contains the file name length and file length (in bytes).
|
||||
/// </summary>
|
||||
public class FileCompressor
|
||||
{
|
||||
/// <summary>
|
||||
/// Compresses all artifacts located at a specified directory.
|
||||
/// </summary>
|
||||
/// <param name="directoryPath">The directory containing the artifacts.</param>
|
||||
/// <param name="archiveFilePath">The file path at which the archive will be created.</param>
|
||||
/// <returns>Returns true if the directory was found and compressed. Returns false otherwise.</returns>
|
||||
public static bool Compress(string directoryPath, string archiveFilePath)
|
||||
{
|
||||
if (!directoryPath.EndsWith("/") && !directoryPath.EndsWith("\\"))
|
||||
directoryPath += Path.DirectorySeparatorChar.ToString();
|
||||
|
||||
var directory = new DirectoryInfo(directoryPath);
|
||||
if (!directory.Exists)
|
||||
return false;
|
||||
|
||||
var files = directory.GetFiles("*", SearchOption.AllDirectories);
|
||||
files = files.Where(x => (File.GetAttributes(x.FullName) & FileAttributes.Hidden) != FileAttributes.Hidden && x.Extension != ".sbpGz").ToArray();
|
||||
if (files.Length == 0)
|
||||
return false;
|
||||
|
||||
using (var archiveStream = new FileStream(archiveFilePath, FileMode.OpenOrCreate, FileAccess.Write))
|
||||
{
|
||||
using (var gZipStream = new GZipStream(archiveStream, CompressionMode.Compress))
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
var relativePath = file.FullName.Substring(directory.FullName.Length);
|
||||
relativePath = relativePath.Replace("\\", "/");
|
||||
|
||||
using (var fileStream = file.OpenRead())
|
||||
{
|
||||
byte[] filePathBytes = Encoding.ASCII.GetBytes(relativePath);
|
||||
|
||||
// Write chunk header
|
||||
gZipStream.Write(BitConverter.GetBytes(filePathBytes.Length), 0, sizeof(int));
|
||||
gZipStream.Write(BitConverter.GetBytes(fileStream.Length), 0, sizeof(long));
|
||||
|
||||
// Write chunk body
|
||||
// Write file path
|
||||
gZipStream.Write(filePathBytes, 0, filePathBytes.Length);
|
||||
|
||||
// Write file contents
|
||||
byte[] readBuffer = new byte[4096];
|
||||
int readSize = readBuffer.Length;
|
||||
while (readSize == readBuffer.Length)
|
||||
{
|
||||
readSize = fileStream.Read(readBuffer, 0, readBuffer.Length);
|
||||
gZipStream.Write(readBuffer, 0, readSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all artifacts compressed in an archive.
|
||||
/// </summary>
|
||||
/// <param name="archiveFilePath">The archive to decompress.</param>
|
||||
/// <param name="directoryPath">The path where the extracted artifacts will be stored.</param>
|
||||
/// <returns>Returns true if the archive was found and decompressed. Returns false otherwise.</returns>
|
||||
public static bool Decompress(string archiveFilePath, string directoryPath)
|
||||
{
|
||||
var archiveFile = new FileInfo(archiveFilePath);
|
||||
if (!archiveFile.Exists)
|
||||
return false;
|
||||
|
||||
var directory = new DirectoryInfo(directoryPath);
|
||||
if (!directory.Exists)
|
||||
directory.Create();
|
||||
|
||||
using (var archiveStream = archiveFile.OpenRead())
|
||||
{
|
||||
using (var gZipStream = new GZipStream(archiveStream, CompressionMode.Decompress))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Read chunk header
|
||||
byte[] header = new byte[sizeof(int) + sizeof(long)];
|
||||
int readSize = gZipStream.Read(header, 0, header.Length);
|
||||
if (readSize != header.Length)
|
||||
break;
|
||||
|
||||
int filePathLength = BitConverter.ToInt32(header, 0);
|
||||
long fileLenth = BitConverter.ToInt64(header, sizeof(int));
|
||||
|
||||
// Read chunk body
|
||||
// Read file path
|
||||
byte[] filePathBytes = new byte[filePathLength];
|
||||
gZipStream.Read(filePathBytes, 0, filePathLength);
|
||||
string filePath = Encoding.ASCII.GetString(filePathBytes);
|
||||
|
||||
var pathSeperator = filePath.LastIndexOf("/");
|
||||
if (pathSeperator > -1)
|
||||
Directory.CreateDirectory(string.Format("{0}/{1}", directoryPath, filePath.Substring(0, pathSeperator)));
|
||||
|
||||
// Read file contents
|
||||
using (var fileStream = new FileStream(directoryPath + "/" + filePath, FileMode.OpenOrCreate, FileAccess.Write))
|
||||
{
|
||||
byte[] readBuffer = new byte[4096];
|
||||
long readRemaining = fileLenth;
|
||||
while (readRemaining > 0)
|
||||
{
|
||||
readSize = readBuffer.Length < readRemaining ? readBuffer.Length : (int)readRemaining;
|
||||
gZipStream.Read(readBuffer, 0, readSize);
|
||||
fileStream.Write(readBuffer, 0, readSize);
|
||||
readRemaining -= readSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 05b619d8a4e3fce47acecb2fcf4122b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,152 @@
|
|||
using UnityEditor.Build.Content;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
#if !UNITY_2019_1_OR_NEWER
|
||||
using System.Reflection;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
using Object = UnityEngine.Object;
|
||||
#endif
|
||||
|
||||
static class GraphicsSettingsApi
|
||||
{
|
||||
#if UNITY_2019_1_OR_NEWER
|
||||
internal static BuildUsageTagGlobal GetGlobalUsage()
|
||||
{
|
||||
return ContentBuildInterface.GetGlobalUsageFromGraphicsSettings();
|
||||
}
|
||||
|
||||
#else
|
||||
static SerializedObject m_SerializedObject;
|
||||
static SerializedProperty m_LightmapStripping;
|
||||
static SerializedProperty m_LightmapKeepPlain;
|
||||
static SerializedProperty m_LightmapKeepDirCombined;
|
||||
static SerializedProperty m_LightmapKeepDynamicPlain;
|
||||
static SerializedProperty m_LightmapKeepDynamicDirCombined;
|
||||
static SerializedProperty m_LightmapKeepShadowMask;
|
||||
static SerializedProperty m_LightmapKeepSubtractive;
|
||||
static SerializedProperty m_FogStripping;
|
||||
static SerializedProperty m_FogKeepLinear;
|
||||
static SerializedProperty m_FogKeepExp;
|
||||
static SerializedProperty m_FogKeepExp2;
|
||||
static SerializedProperty m_InstancingStripping;
|
||||
|
||||
static FieldInfo m_LightmapModesUsed;
|
||||
static FieldInfo m_LegacyLightmapModesUsed;
|
||||
static FieldInfo m_DynamicLightmapsUsed;
|
||||
static FieldInfo m_FogModesUsed;
|
||||
static FieldInfo m_ForceInstancingStrip;
|
||||
static FieldInfo m_ForceInstancingKeep;
|
||||
static FieldInfo m_ShadowMasksUsed;
|
||||
static FieldInfo m_SubtractiveUsed;
|
||||
|
||||
static uint m_LightmapModesUsed_Value;
|
||||
static uint m_LegacyLightmapModesUsed_Value;
|
||||
static uint m_DynamicLightmapsUsed_Value;
|
||||
static uint m_FogModesUsed_Value;
|
||||
static bool m_ForceInstancingStrip_Value;
|
||||
static bool m_ForceInstancingKeep_Value;
|
||||
static bool m_ShadowMasksUsed_Value;
|
||||
static bool m_SubtractiveUsed_Value;
|
||||
|
||||
static void OnEnable()
|
||||
{
|
||||
m_LightmapModesUsed_Value = 0;
|
||||
m_LegacyLightmapModesUsed_Value = 0;
|
||||
m_DynamicLightmapsUsed_Value = 0;
|
||||
m_FogModesUsed_Value = 0;
|
||||
m_ForceInstancingStrip_Value = false;
|
||||
m_ForceInstancingKeep_Value = false;
|
||||
m_ShadowMasksUsed_Value = false;
|
||||
m_SubtractiveUsed_Value = false;
|
||||
|
||||
if (m_SerializedObject != null)
|
||||
return;
|
||||
|
||||
var getGraphicsSettings = typeof(GraphicsSettings).GetMethod("GetGraphicsSettings", BindingFlags.Static | BindingFlags.NonPublic);
|
||||
var graphicsSettings = getGraphicsSettings.Invoke(null, null) as Object;
|
||||
m_SerializedObject = new SerializedObject(graphicsSettings);
|
||||
|
||||
m_LightmapStripping = m_SerializedObject.FindProperty("m_LightmapStripping");
|
||||
m_LightmapKeepPlain = m_SerializedObject.FindProperty("m_LightmapKeepPlain");
|
||||
m_LightmapKeepDirCombined = m_SerializedObject.FindProperty("m_LightmapKeepDirCombined");
|
||||
m_LightmapKeepDynamicPlain = m_SerializedObject.FindProperty("m_LightmapKeepDynamicPlain");
|
||||
m_LightmapKeepDynamicDirCombined = m_SerializedObject.FindProperty("m_LightmapKeepDynamicDirCombined");
|
||||
m_LightmapKeepShadowMask = m_SerializedObject.FindProperty("m_LightmapKeepShadowMask");
|
||||
m_LightmapKeepSubtractive = m_SerializedObject.FindProperty("m_LightmapKeepSubtractive");
|
||||
m_FogStripping = m_SerializedObject.FindProperty("m_FogStripping");
|
||||
m_FogKeepLinear = m_SerializedObject.FindProperty("m_FogKeepLinear");
|
||||
m_FogKeepExp = m_SerializedObject.FindProperty("m_FogKeepExp");
|
||||
m_FogKeepExp2 = m_SerializedObject.FindProperty("m_FogKeepExp2");
|
||||
m_InstancingStripping = m_SerializedObject.FindProperty("m_InstancingStripping");
|
||||
|
||||
var globalUsageType = typeof(BuildUsageTagGlobal);
|
||||
m_LightmapModesUsed = globalUsageType.GetField("m_LightmapModesUsed", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
m_LegacyLightmapModesUsed = globalUsageType.GetField("m_LegacyLightmapModesUsed", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
m_DynamicLightmapsUsed = globalUsageType.GetField("m_DynamicLightmapsUsed", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
m_FogModesUsed = globalUsageType.GetField("m_FogModesUsed", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
m_ForceInstancingStrip = globalUsageType.GetField("m_ForceInstancingStrip", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
m_ForceInstancingKeep = globalUsageType.GetField("m_ForceInstancingKeep", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
m_ShadowMasksUsed = globalUsageType.GetField("m_ShadowMasksUsed", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
m_SubtractiveUsed = globalUsageType.GetField("m_SubtractiveUsed", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
}
|
||||
|
||||
internal static BuildUsageTagGlobal GetGlobalUsage()
|
||||
{
|
||||
OnEnable();
|
||||
m_SerializedObject.Update();
|
||||
|
||||
if (m_LightmapStripping.intValue != 0)
|
||||
{
|
||||
if (m_LightmapKeepPlain.boolValue)
|
||||
m_LegacyLightmapModesUsed_Value |= (1 << 0);
|
||||
if (m_LightmapKeepDirCombined.boolValue)
|
||||
m_LegacyLightmapModesUsed_Value |= (1 << 2);
|
||||
|
||||
if (m_LightmapKeepPlain.boolValue)
|
||||
m_LightmapModesUsed_Value |= (1 << 0);
|
||||
if (m_LightmapKeepDirCombined.boolValue)
|
||||
m_LightmapModesUsed_Value |= (1 << 1);
|
||||
|
||||
if (m_LightmapKeepDynamicPlain.boolValue)
|
||||
m_DynamicLightmapsUsed_Value |= (1 << 0);
|
||||
if (m_LightmapKeepDynamicDirCombined.boolValue)
|
||||
m_DynamicLightmapsUsed_Value |= (1 << 1);
|
||||
|
||||
if (m_LightmapKeepShadowMask.boolValue)
|
||||
m_ShadowMasksUsed_Value = true;
|
||||
if (m_LightmapKeepSubtractive.boolValue)
|
||||
m_SubtractiveUsed_Value = true;
|
||||
}
|
||||
|
||||
if (m_FogStripping.intValue != 0)
|
||||
{
|
||||
if (m_FogKeepLinear.boolValue)
|
||||
m_FogModesUsed_Value |= (1 << 1);
|
||||
if (m_FogKeepExp.boolValue)
|
||||
m_FogModesUsed_Value |= (1 << 2);
|
||||
if (m_FogKeepExp2.boolValue)
|
||||
m_FogModesUsed_Value |= (1 << 3);
|
||||
}
|
||||
|
||||
m_ForceInstancingStrip_Value = (m_InstancingStripping.intValue == 1);
|
||||
m_ForceInstancingKeep_Value = (m_InstancingStripping.intValue == 2);
|
||||
|
||||
BuildUsageTagGlobal globalUsage = new BuildUsageTagGlobal();
|
||||
var boxedUsage = (object)globalUsage;
|
||||
m_LightmapModesUsed.SetValue(boxedUsage, m_LightmapModesUsed_Value);
|
||||
m_LegacyLightmapModesUsed.SetValue(boxedUsage, m_LegacyLightmapModesUsed_Value);
|
||||
m_DynamicLightmapsUsed.SetValue(boxedUsage, m_DynamicLightmapsUsed_Value);
|
||||
m_FogModesUsed.SetValue(boxedUsage, m_FogModesUsed_Value);
|
||||
m_ForceInstancingStrip.SetValue(boxedUsage, m_ForceInstancingStrip_Value);
|
||||
m_ForceInstancingKeep.SetValue(boxedUsage, m_ForceInstancingKeep_Value);
|
||||
m_ShadowMasksUsed.SetValue(boxedUsage, m_ShadowMasksUsed_Value);
|
||||
m_SubtractiveUsed.SetValue(boxedUsage, m_SubtractiveUsed_Value);
|
||||
globalUsage = (BuildUsageTagGlobal)boxedUsage;
|
||||
return globalUsage;
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 07feb49ed5894cd459f42e594a2fbb94
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,44 @@
|
|||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
internal class HashStream : Stream
|
||||
{
|
||||
HashAlgorithm m_Algorithm;
|
||||
|
||||
public HashStream(HashAlgorithm algorithm)
|
||||
{
|
||||
m_Algorithm = algorithm;
|
||||
}
|
||||
|
||||
public RawHash GetHash()
|
||||
{
|
||||
m_Algorithm.TransformFinalBlock(System.Array.Empty<byte>(), 0, 0);
|
||||
return new RawHash(m_Algorithm.Hash);
|
||||
}
|
||||
|
||||
public override bool CanRead => false;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length => Null.Length;
|
||||
|
||||
public override long Position { get => Null.Position; set => Null.Position = value; }
|
||||
|
||||
public override void Flush() {}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => Null.Read(buffer, offset, count);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => Null.Seek(offset, origin);
|
||||
|
||||
public override void SetLength(long value) => Null.SetLength(value);
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
m_Algorithm.TransformBlock(buffer, offset, count, null, 0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3b86965c40eee0449a9c0a7db2f0e759
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,153 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor.Build.Content;
|
||||
using UnityEditor.Build.Pipeline.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
static class HashingHelpers
|
||||
{
|
||||
static void WriteObjectIdentifier(ObjectIdentifier obj, BinaryWriter writer)
|
||||
{
|
||||
writer.Write(obj.filePath ?? string.Empty);
|
||||
writer.Write((int)obj.fileType);
|
||||
writer.Write(obj.guid.ToString());
|
||||
writer.Write(obj.localIdentifierInFile);
|
||||
}
|
||||
|
||||
internal static void WriteObjectIdentifiers(List<ObjectIdentifier> ids, BinaryWriter writer)
|
||||
{
|
||||
writer.Write((ids != null) ? ids.Count : 0);
|
||||
if (ids != null)
|
||||
foreach (ObjectIdentifier o in ids)
|
||||
WriteObjectIdentifier(o, writer);
|
||||
}
|
||||
|
||||
internal static void WriteHashData(AssetLoadInfo info, BinaryWriter writer)
|
||||
{
|
||||
writer.Write(info.address ?? string.Empty);
|
||||
writer.Write(info.asset.ToString());
|
||||
WriteObjectIdentifiers(info.referencedObjects, writer);
|
||||
WriteObjectIdentifiers(info.includedObjects, writer);
|
||||
}
|
||||
|
||||
internal static void WriteHashData(AssetBundleInfo info, BinaryWriter writer)
|
||||
{
|
||||
if (info != null)
|
||||
{
|
||||
writer.Write(info.bundleName ?? string.Empty);
|
||||
if (info.bundleAssets != null)
|
||||
foreach (AssetLoadInfo ali in info.bundleAssets)
|
||||
WriteHashData(ali, writer);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WriteHashData(SceneLoadInfo info, BinaryWriter writer)
|
||||
{
|
||||
writer.Write(info.asset.ToString());
|
||||
writer.Write(info.address ?? string.Empty);
|
||||
writer.Write(info.internalName ?? string.Empty);
|
||||
}
|
||||
|
||||
internal static void WriteHashData(SceneBundleInfo info, BinaryWriter writer)
|
||||
{
|
||||
if (info != null)
|
||||
{
|
||||
writer.Write(info.bundleName ?? string.Empty);
|
||||
if (info.bundleScenes != null)
|
||||
foreach (SceneLoadInfo sli in info.bundleScenes)
|
||||
WriteHashData(sli, writer);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WriteHashData(PreloadInfo info, BinaryWriter writer)
|
||||
{
|
||||
if (info != null)
|
||||
WriteObjectIdentifiers(info.preloadObjects, writer);
|
||||
}
|
||||
|
||||
internal static void WriteHashData(SerializationInfo info, BinaryWriter writer)
|
||||
{
|
||||
WriteObjectIdentifier(info.serializationObject, writer);
|
||||
writer.Write(info.serializationIndex);
|
||||
}
|
||||
|
||||
internal static void WriteHashData(WriteCommand cmd, BinaryWriter writer)
|
||||
{
|
||||
if (cmd != null)
|
||||
{
|
||||
writer.Write(cmd.fileName ?? string.Empty);
|
||||
writer.Write(cmd.internalName ?? string.Empty);
|
||||
if (cmd.serializeObjects != null)
|
||||
{
|
||||
cmd.serializeObjects.ForEach((x) => WriteHashData(x, writer));
|
||||
foreach (SerializationInfo info in cmd.serializeObjects)
|
||||
WriteHashData(info, writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Hash128 GetHash128(this SerializationInfo info)
|
||||
{
|
||||
StreamHasher hasher = new StreamHasher();
|
||||
HashingHelpers.WriteHashData(info, hasher.Writer);
|
||||
return hasher.GetHash();
|
||||
}
|
||||
|
||||
public static Hash128 GetHash128(this PreloadInfo info)
|
||||
{
|
||||
StreamHasher hasher = new StreamHasher();
|
||||
HashingHelpers.WriteHashData(info, hasher.Writer);
|
||||
return hasher.GetHash();
|
||||
}
|
||||
|
||||
public static Hash128 GetHash128(this AssetBundleInfo info)
|
||||
{
|
||||
StreamHasher hasher = new StreamHasher();
|
||||
HashingHelpers.WriteHashData(info, hasher.Writer);
|
||||
return hasher.GetHash();
|
||||
}
|
||||
|
||||
public static Hash128 GetHash128(this SceneBundleInfo info)
|
||||
{
|
||||
StreamHasher hasher = new StreamHasher();
|
||||
HashingHelpers.WriteHashData(info, hasher.Writer);
|
||||
return hasher.GetHash();
|
||||
}
|
||||
|
||||
public static Hash128 GetHash128(this AssetLoadInfo info)
|
||||
{
|
||||
StreamHasher hasher = new StreamHasher();
|
||||
HashingHelpers.WriteHashData(info, hasher.Writer);
|
||||
return hasher.GetHash();
|
||||
}
|
||||
|
||||
public static Hash128 GetHash128(this WriteCommand cmd)
|
||||
{
|
||||
StreamHasher hasher = new StreamHasher();
|
||||
HashingHelpers.WriteHashData(cmd, hasher.Writer);
|
||||
return hasher.GetHash();
|
||||
}
|
||||
|
||||
public static Hash128 GetHash128(this BuildSettings settings)
|
||||
{
|
||||
return HashingMethods.Calculate(settings.target, settings.group, settings.buildFlags).ToHash128();
|
||||
}
|
||||
}
|
||||
|
||||
internal class StreamHasher
|
||||
{
|
||||
HashStream m_Stream;
|
||||
public BinaryWriter Writer { private set; get; }
|
||||
|
||||
public StreamHasher()
|
||||
{
|
||||
var hasher = HashingMethods.GetHashAlgorithm();
|
||||
m_Stream = new HashStream(hasher);
|
||||
Writer = new BinaryWriter(m_Stream);
|
||||
}
|
||||
|
||||
public Hash128 GetHash()
|
||||
{
|
||||
return m_Stream.GetHash().ToHash128();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8597e625afbd18f4fa730ad9b5507d61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,507 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores hash information as an array of bytes.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct RawHash : IEquatable<RawHash>
|
||||
{
|
||||
readonly byte[] m_Hash;
|
||||
|
||||
internal RawHash(byte[] hash)
|
||||
{
|
||||
m_Hash = hash;
|
||||
}
|
||||
|
||||
internal static RawHash Zero()
|
||||
{
|
||||
return new RawHash(new byte[16]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the hash to bytes.
|
||||
/// </summary>
|
||||
/// <returns>Returns the converted hash as an array of bytes.</returns>
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
return m_Hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the hash to <see cref="Hash128"/> format.
|
||||
/// </summary>
|
||||
/// <returns>Returns the converted hash.</returns>
|
||||
public Hash128 ToHash128()
|
||||
{
|
||||
if (m_Hash == null || m_Hash.Length != 16)
|
||||
return new Hash128();
|
||||
|
||||
return new Hash128(BitConverter.ToUInt32(m_Hash, 0), BitConverter.ToUInt32(m_Hash, 4),
|
||||
BitConverter.ToUInt32(m_Hash, 8), BitConverter.ToUInt32(m_Hash, 12));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the hash to a guid.
|
||||
/// </summary>
|
||||
/// <returns>Returns the converted hash as a guid.</returns>
|
||||
public GUID ToGUID()
|
||||
{
|
||||
if (m_Hash == null || m_Hash.Length != 16)
|
||||
return new GUID();
|
||||
|
||||
return new GUID(ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the hash to a formatted string.
|
||||
/// </summary>
|
||||
/// <returns>Returns the hash as a string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
if (m_Hash == null || m_Hash.Length != 16)
|
||||
return "00000000000000000000000000000000";
|
||||
|
||||
return BitConverter.ToString(m_Hash).Replace("-", "").ToLower();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the current hash instance is equivalent to the specified hash.
|
||||
/// </summary>
|
||||
/// <param name="other">The hash to compare to.</param>
|
||||
/// <returns>Returns true if the hashes are equivalent. Returns false otherwise.</returns>
|
||||
public bool Equals(RawHash other)
|
||||
{
|
||||
return m_Hash.SequenceEqual(other.m_Hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the current hash instance is equivalent to the specified hash.
|
||||
/// </summary>
|
||||
/// <param name="obj">The hash to compare to.</param>
|
||||
/// <returns>Returns true if the hashes are equivalent. Returns false otherwise.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
return obj is RawHash && Equals((RawHash)obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash code for the cache entry.
|
||||
/// </summary>
|
||||
/// <returns>Returns the hash code for the cache entry.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (m_Hash != null ? m_Hash.GetHashCode() : 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the left hash instance is equivalent to the right hash.
|
||||
/// </summary>
|
||||
/// <param name="left">The hash to compare against.</param>
|
||||
/// <param name="right">The hash to compare to.</param>
|
||||
/// <returns>Returns true if the hashes are equivalent. Returns false otherwise.</returns>
|
||||
public static bool operator==(RawHash left, RawHash right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the left hash instance is not equivalent to the right hash.
|
||||
/// </summary>
|
||||
/// <param name="left">The hash to compare against.</param>
|
||||
/// <param name="right">The hash to compare to.</param>
|
||||
/// <returns>Returns true if the hashes are not equivalent. Returns false otherwise.</returns>
|
||||
public static bool operator!=(RawHash left, RawHash right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="RawHash"/> for an object.
|
||||
/// </summary>
|
||||
public static class HashingMethods
|
||||
{
|
||||
// TODO: Make this even faster!
|
||||
// Maybe use unsafe code to access the raw bytes and pass them directly into the stream?
|
||||
// Maybe pass the bytes into the HashAlgorithm directly
|
||||
// TODO: Does this handle arrays?
|
||||
static void GetRawBytes(Stack<object> state, Stream stream)
|
||||
{
|
||||
if (state.Count == 0)
|
||||
return;
|
||||
|
||||
object currObj = state.Pop();
|
||||
if (currObj == null)
|
||||
return;
|
||||
|
||||
// Handle basic types
|
||||
if (currObj is bool)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((bool)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is char)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((char)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is double)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((double)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is short)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((short)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is int)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((int)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is long)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((long)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is float)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((float)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is ushort)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((ushort)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is uint)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((uint)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is ulong)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes((ulong)currObj);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is byte[])
|
||||
{
|
||||
var bytes = (byte[])currObj;
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is string)
|
||||
{
|
||||
byte[] bytes;
|
||||
var str = (string)currObj;
|
||||
if (str.Any(c => c > 127))
|
||||
bytes = Encoding.Unicode.GetBytes(str);
|
||||
else
|
||||
bytes = Encoding.ASCII.GetBytes(str);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else if (currObj is GUID)
|
||||
{
|
||||
byte[] hashBytes = new byte[16];
|
||||
IntPtr ptr = Marshal.AllocHGlobal(16);
|
||||
Marshal.StructureToPtr((GUID)currObj, ptr, false);
|
||||
Marshal.Copy(ptr, hashBytes, 0, 16);
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
PassBytesToStreamInBlocks(stream, hashBytes, 4);
|
||||
}
|
||||
else if (currObj is Hash128)
|
||||
{
|
||||
byte[] hashBytes = new byte[16];
|
||||
IntPtr ptr = Marshal.AllocHGlobal(16);
|
||||
Marshal.StructureToPtr((Hash128)currObj, ptr, false);
|
||||
Marshal.Copy(ptr, hashBytes, 0, 16);
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
PassBytesToStreamInBlocks(stream, hashBytes, 4);
|
||||
}
|
||||
else if (currObj.GetType().IsEnum)
|
||||
{
|
||||
// Handle enums
|
||||
var type = Enum.GetUnderlyingType(currObj.GetType());
|
||||
var newObj = Convert.ChangeType(currObj, type);
|
||||
state.Push(newObj);
|
||||
}
|
||||
else if (currObj.GetType().IsArray)
|
||||
{
|
||||
// Handle arrays
|
||||
var array = (Array)currObj;
|
||||
for (int i = array.Length - 1; i >= 0; i--)
|
||||
state.Push(array.GetValue(i));
|
||||
}
|
||||
else if (currObj is System.Collections.IEnumerable)
|
||||
{
|
||||
var iterator = (System.Collections.IEnumerable)currObj;
|
||||
var reverseOrder = new Stack<object>();
|
||||
foreach (var newObj in iterator)
|
||||
reverseOrder.Push(newObj);
|
||||
foreach (var newObj in reverseOrder)
|
||||
state.Push(newObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use reflection for remainder
|
||||
var fields = currObj.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
for (var index = fields.Length - 1; index >= 0; index--)
|
||||
{
|
||||
var field = fields[index];
|
||||
var newObj = field.GetValue(currObj);
|
||||
state.Push(newObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void GetRawBytes(Stream stream, object obj)
|
||||
{
|
||||
var objStack = new Stack<object>();
|
||||
objStack.Push(obj);
|
||||
while (objStack.Count > 0)
|
||||
GetRawBytes(objStack, stream);
|
||||
}
|
||||
|
||||
static void GetRawBytes(Stream stream, params object[] objects)
|
||||
{
|
||||
var objStack = new Stack<object>();
|
||||
for (var index = objects.Length - 1; index >= 0; index--)
|
||||
objStack.Push(objects[index]);
|
||||
while (objStack.Count > 0)
|
||||
GetRawBytes(objStack, stream);
|
||||
}
|
||||
|
||||
internal static HashAlgorithm GetHashAlgorithm()
|
||||
{
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
// New projects on 2021.1 will default useSpookyHash to true
|
||||
// Upgraded projects will remain false until they choose to switch
|
||||
return ScriptableBuildPipeline.useV2Hasher ? (HashAlgorithm)SpookyHash.Create() : new MD5CryptoServiceProvider();
|
||||
#else
|
||||
return new MD5CryptoServiceProvider();
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static HashAlgorithm GetHashAlgorithm(Type type)
|
||||
{
|
||||
if (type == null)
|
||||
{
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
// New projects on 2021.1 will default useSpookyHash to true
|
||||
// Upgraded projects will remain false until they choose to switch
|
||||
type = ScriptableBuildPipeline.useV2Hasher ? typeof(SpookyHash) : typeof(MD5);
|
||||
#else
|
||||
type = typeof(MD5);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (type == typeof(MD4))
|
||||
return MD4.Create();
|
||||
#if UNITY_2019_3_OR_NEWER
|
||||
if (type == typeof(SpookyHash))
|
||||
return SpookyHash.Create();
|
||||
#endif
|
||||
|
||||
// TODO: allow user created HashAlgorithms?
|
||||
var alggorithm = HashAlgorithm.Create(type.FullName);
|
||||
if (alggorithm == null)
|
||||
throw new NotImplementedException("Unable to create hash algorithm: '" + type.FullName + "'.");
|
||||
return alggorithm;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash for a stream of data.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream of data.</param>
|
||||
/// <returns>Returns the hash of the stream.</returns>
|
||||
public static RawHash CalculateStream(Stream stream)
|
||||
{
|
||||
if (stream == null)
|
||||
return RawHash.Zero();
|
||||
if (stream is HashStream hs)
|
||||
return hs.GetHash();
|
||||
|
||||
byte[] hash;
|
||||
using (var hashAlgorithm = GetHashAlgorithm())
|
||||
hash = hashAlgorithm.ComputeHash(stream);
|
||||
return new RawHash(hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash for a stream of data.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The hash algorithm type.</typeparam>
|
||||
/// <param name="stream">The stream of data.</param>
|
||||
/// <returns>Returns the hash of the stream.</returns>
|
||||
public static RawHash CalculateStream<T>(Stream stream) where T : HashAlgorithm
|
||||
{
|
||||
if (stream == null)
|
||||
return RawHash.Zero();
|
||||
if (stream is HashStream hs)
|
||||
return hs.GetHash();
|
||||
|
||||
byte[] hash;
|
||||
using (var hashAlgorithm = GetHashAlgorithm(typeof(T)))
|
||||
hash = hashAlgorithm.ComputeHash(stream);
|
||||
return new RawHash(hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash for an object.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <returns>Returns the hash of the object.</returns>
|
||||
public static RawHash Calculate(object obj)
|
||||
{
|
||||
RawHash rawHash;
|
||||
using (var stream = new HashStream(GetHashAlgorithm()))
|
||||
{
|
||||
GetRawBytes(stream, obj);
|
||||
rawHash = stream.GetHash();
|
||||
}
|
||||
return rawHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash for a set of objects.
|
||||
/// </summary>
|
||||
/// <param name="objects">The objects.</param>
|
||||
/// <returns>Returns the hash of the set of objects.</returns>
|
||||
public static RawHash Calculate(params object[] objects)
|
||||
{
|
||||
if (objects == null)
|
||||
return RawHash.Zero();
|
||||
|
||||
RawHash rawHash;
|
||||
using (var stream = new HashStream(GetHashAlgorithm()))
|
||||
{
|
||||
GetRawBytes(stream, objects);
|
||||
rawHash = stream.GetHash();
|
||||
}
|
||||
return rawHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash for a pair of Hash128 objects. Optimized specialization of the generic Calculate() methods that has been shown to be ~3x faster
|
||||
/// The generic function uses reflection to obtain the four 32bit fields in the Hash128 which is slow, this function uses more direct byte access
|
||||
/// </summary>
|
||||
/// <param name="hash1">The first hash to combine</param>
|
||||
/// <param name="hash2">The second hash to combine</param>
|
||||
/// <returns>Returns the combined hash of the two hashes.</returns>
|
||||
public static RawHash Calculate(Hash128 hash1, Hash128 hash2)
|
||||
{
|
||||
byte[] hashBytes = new byte[32];
|
||||
IntPtr ptr = Marshal.AllocHGlobal(16);
|
||||
|
||||
Marshal.StructureToPtr(hash1, ptr, false);
|
||||
Marshal.Copy(ptr, hashBytes, 0, 16);
|
||||
|
||||
Marshal.StructureToPtr(hash2, ptr, true);
|
||||
Marshal.Copy(ptr, hashBytes, 16, 16);
|
||||
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
|
||||
HashStream hashStream = new HashStream(GetHashAlgorithm());
|
||||
PassBytesToStreamInBlocks(hashStream, hashBytes, 4);
|
||||
return hashStream.GetHash();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send bytes from an array to a stream as a sequence of blocks
|
||||
/// Not all hash algorithms behave the same passing data as one large block instead of multiple smaller ones so produce different hashes if
|
||||
/// we pass a GUID or Hash128 as a single 16 byte block for example instead of the 4x4 byte blocks the generic hashing code produces for these types
|
||||
/// This function passes bytes from a buffer in chunks of a specified size to ensure we get the same hash from the same data in such cases
|
||||
/// Our SpookyHash implementation suffers from this for example (see SpookyHash::Short()) while MD5 does not
|
||||
/// </summary>
|
||||
/// <param name="stream">Stream to write bytes to</param>
|
||||
/// <param name="byteBlock">Array of bytes to pass to the stream, must be multiple of blockSizeBytes in size or an InvalidOperationException will be thrown</param>
|
||||
/// <param name="blockSizeBytes">Number of bytes to write to the stream each time</param>
|
||||
/// <returns></returns>
|
||||
static void PassBytesToStreamInBlocks(Stream stream, byte[] bytesToWrite, int blockSizeBytes)
|
||||
{
|
||||
if ((bytesToWrite.Length % blockSizeBytes) != 0)
|
||||
throw new InvalidOperationException($"PassBytesToStreamInBlocks() byte array size of {bytesToWrite.Length} is required to be a multiple of {blockSizeBytes} which it's not");
|
||||
|
||||
for (int offset = 0; offset < bytesToWrite.Length; offset += blockSizeBytes)
|
||||
{
|
||||
stream.Write(bytesToWrite, offset, blockSizeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash for an object.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The hash algorithm type.</typeparam>
|
||||
/// <param name="obj">The object.</param>
|
||||
/// <returns>Returns the hash of the object.</returns>
|
||||
public static RawHash Calculate<T>(object obj) where T : HashAlgorithm
|
||||
{
|
||||
RawHash rawHash;
|
||||
using (var stream = new HashStream(GetHashAlgorithm(typeof(T))))
|
||||
{
|
||||
GetRawBytes(stream, obj);
|
||||
rawHash = stream.GetHash();
|
||||
}
|
||||
return rawHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash for a set of objects.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The hash algorithm type.</typeparam>
|
||||
/// <param name="objects">The objects.</param>
|
||||
/// <returns>Returns the hash of the set of objects.</returns>
|
||||
public static RawHash Calculate<T>(params object[] objects) where T : HashAlgorithm
|
||||
{
|
||||
if (objects == null)
|
||||
return RawHash.Zero();
|
||||
|
||||
RawHash rawHash;
|
||||
using (var stream = new HashStream(GetHashAlgorithm(typeof(T))))
|
||||
{
|
||||
GetRawBytes(stream, objects);
|
||||
rawHash = stream.GetHash();
|
||||
}
|
||||
return rawHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash for a file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The file path.</param>
|
||||
/// <returns>Returns the hash of the file.</returns>
|
||||
public static RawHash CalculateFile(string filePath)
|
||||
{
|
||||
RawHash rawHash;
|
||||
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||
rawHash = CalculateStream(stream);
|
||||
return rawHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the hash for a file.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The hash algorithm type.</typeparam>
|
||||
/// <param name="filePath">The file path.</param>
|
||||
/// <returns>Returns the hash of the file.</returns>
|
||||
public static RawHash CalculateFile<T>(string filePath) where T : HashAlgorithm
|
||||
{
|
||||
RawHash rawHash;
|
||||
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||
rawHash = CalculateStream<T>(stream);
|
||||
return rawHash;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f72b7476a69b89147a08603d7391218a
|
||||
timeCreated: 1504709534
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,272 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Xml;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// This can be used to create a LinkXml for your build. This will ensure that the desired runtime types are packed into the build.
|
||||
/// </summary>
|
||||
public class LinkXmlGenerator
|
||||
{
|
||||
Dictionary<Type, Type> m_TypeConversion = new Dictionary<Type, Type>();
|
||||
HashSet<Type> m_Types = new HashSet<Type>();
|
||||
HashSet<Assembly> m_Assemblies = new HashSet<Assembly>();
|
||||
|
||||
/// <summary>
|
||||
/// Obsolete, no longer does anything.
|
||||
/// </summary>
|
||||
[Obsolete] protected Dictionary<string, HashSet<string>> serializedClassesPerAssembly = null;
|
||||
Dictionary<string, HashSet<string>> m_SerializedClassesPerAssembly = new Dictionary<string, HashSet<string>>();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs and returns a LinkXmlGenerator object that contains default UnityEditor to UnityEngine type conversions.
|
||||
/// </summary>
|
||||
/// <returns>LinkXmlGenerator object with the default UnityEngine type conversions.</returns>
|
||||
public static LinkXmlGenerator CreateDefault()
|
||||
{
|
||||
var linker = new LinkXmlGenerator();
|
||||
var types = GetEditorTypeConversions();
|
||||
foreach (var pair in types)
|
||||
linker.SetTypeConversion(pair.Key, pair.Value);
|
||||
return linker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the set of UnityEditor types that have valid runtime direct mappings.
|
||||
/// </summary>
|
||||
/// <returns>Array of KeyValuePairs containing the editor type and it's equivalent runtime type.</returns>
|
||||
public static KeyValuePair<Type, Type>[] GetEditorTypeConversions()
|
||||
{
|
||||
var editor = Assembly.GetAssembly(typeof(UnityEditor.BuildPipeline));
|
||||
return new[]
|
||||
{
|
||||
new KeyValuePair<Type, Type>(typeof(UnityEditor.Animations.AnimatorController), typeof(UnityEngine.RuntimeAnimatorController)),
|
||||
new KeyValuePair<Type, Type>(editor.GetType("UnityEditor.Audio.AudioMixerController"), typeof(UnityEngine.Audio.AudioMixer)),
|
||||
new KeyValuePair<Type, Type>(editor.GetType("UnityEditor.Audio.AudioMixerGroupController"), typeof(UnityEngine.Audio.AudioMixerGroup)),
|
||||
new KeyValuePair<Type, Type>(typeof(UnityEditor.MonoScript), typeof(UnityEngine.Object)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add runtime assembly to the LinkXml Generator.
|
||||
/// </summary>
|
||||
/// <param name="assemblies">The desired runtime assemblies.</param>
|
||||
public void AddAssemblies(params Assembly[] assemblies)
|
||||
{
|
||||
if (assemblies == null)
|
||||
return;
|
||||
foreach (var a in assemblies)
|
||||
AddAssemblyInternal(a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add runtime assembly to the LinkXml Generator.
|
||||
/// </summary>
|
||||
/// <param name="assemblies">The desired runtime assemblies.</param>
|
||||
public void AddAssemblies(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
if (assemblies == null)
|
||||
return;
|
||||
foreach (var a in assemblies)
|
||||
AddAssemblyInternal(a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add runtime type to the LinkXml Generator.
|
||||
/// </summary>
|
||||
/// <param name="types">The desired runtime types.</param>
|
||||
public void AddTypes(params Type[] types)
|
||||
{
|
||||
if (types == null)
|
||||
return;
|
||||
foreach (var t in types)
|
||||
AddTypeInternal(t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add runtime type to the LinkXml Generator.
|
||||
/// </summary>
|
||||
/// <param name="types">The desired runtime types.</param>
|
||||
public void AddTypes(IEnumerable<Type> types)
|
||||
{
|
||||
if (types == null)
|
||||
return;
|
||||
foreach (var t in types)
|
||||
AddTypeInternal(t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add SerializedReference class type from fully qualified name to the Generator, those will end up in PreservedTypes.xml
|
||||
/// </summary>
|
||||
/// <param name="serializedRefTypes">The SerializeReference instance fully qualified name we want to preserve.</param>
|
||||
public void AddSerializedClass(IEnumerable<string> serializedRefTypes)
|
||||
{
|
||||
if (serializedRefTypes == null)
|
||||
return;
|
||||
foreach (var t in serializedRefTypes)
|
||||
{
|
||||
var indexOfAssembly = t.IndexOf(':');
|
||||
if (indexOfAssembly != -1)
|
||||
AddSerializedClassInternal(t.Substring(0, indexOfAssembly), t.Substring(indexOfAssembly + 1, t.Length - (indexOfAssembly + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTypeInternal(Type t)
|
||||
{
|
||||
if (t == null)
|
||||
return;
|
||||
|
||||
Type convertedType;
|
||||
if (m_TypeConversion.TryGetValue(t, out convertedType))
|
||||
m_Types.Add(convertedType);
|
||||
else
|
||||
m_Types.Add(t);
|
||||
}
|
||||
|
||||
private void AddSerializedClassInternal(string assemblyName, string classWithNameSpace)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assemblyName))
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(classWithNameSpace))
|
||||
return;
|
||||
|
||||
if (!m_SerializedClassesPerAssembly.TryGetValue(assemblyName, out HashSet<string> types))
|
||||
m_SerializedClassesPerAssembly[assemblyName] = types = new HashSet<string>();
|
||||
|
||||
types.Add(classWithNameSpace);
|
||||
}
|
||||
|
||||
private void AddAssemblyInternal(Assembly a)
|
||||
{
|
||||
if (a == null)
|
||||
return;
|
||||
|
||||
m_Assemblies.Add(a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup runtime type conversion
|
||||
/// </summary>
|
||||
/// <param name="a">Convert from type.</param>
|
||||
/// <param name="b">Convert to type.</param>
|
||||
public void SetTypeConversion(Type a, Type b)
|
||||
{
|
||||
m_TypeConversion[a] = b;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the LinkXml to the specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to save the linker xml file.</param>
|
||||
public void Save(string path)
|
||||
{
|
||||
var assemblyMap = new Dictionary<Assembly, List<Type>>();
|
||||
foreach (var a in m_Assemblies)
|
||||
{
|
||||
if (!assemblyMap.TryGetValue(a, out _))
|
||||
assemblyMap.Add(a, new List<Type>());
|
||||
}
|
||||
foreach (var t in m_Types)
|
||||
{
|
||||
var a = t.Assembly;
|
||||
List<Type> types;
|
||||
if (!assemblyMap.TryGetValue(a, out types))
|
||||
assemblyMap.Add(a, types = new List<Type>());
|
||||
types.Add(t);
|
||||
}
|
||||
XmlDocument doc = new XmlDocument();
|
||||
var linker = doc.AppendChild(doc.CreateElement("linker"));
|
||||
foreach (var k in assemblyMap.OrderBy(a => a.Key.FullName))
|
||||
{
|
||||
var assembly = linker.AppendChild(doc.CreateElement("assembly"));
|
||||
var attr = doc.CreateAttribute("fullname");
|
||||
attr.Value = k.Key.FullName;
|
||||
if (assembly.Attributes != null)
|
||||
{
|
||||
assembly.Attributes.Append(attr);
|
||||
|
||||
if (m_Assemblies.Contains(k.Key))
|
||||
{
|
||||
var preserveAssembly = doc.CreateAttribute("preserve");
|
||||
preserveAssembly.Value = "all";
|
||||
assembly.Attributes.Append(preserveAssembly);
|
||||
}
|
||||
|
||||
foreach (var t in k.Value.OrderBy(t => t.FullName))
|
||||
{
|
||||
var typeEl = assembly.AppendChild(doc.CreateElement("type"));
|
||||
var tattr = doc.CreateAttribute("fullname");
|
||||
tattr.Value = t.FullName;
|
||||
if (typeEl.Attributes != null)
|
||||
{
|
||||
typeEl.Attributes.Append(tattr);
|
||||
var pattr = doc.CreateAttribute("preserve");
|
||||
pattr.Value = "all";
|
||||
typeEl.Attributes.Append(pattr);
|
||||
}
|
||||
}
|
||||
|
||||
//Add serialize reference classes which are contained in the current assembly
|
||||
var assemblyName = k.Key.GetName().Name;
|
||||
if (m_SerializedClassesPerAssembly.ContainsKey(assemblyName))
|
||||
{
|
||||
//Add content for this
|
||||
foreach (var t in m_SerializedClassesPerAssembly[assemblyName])
|
||||
{
|
||||
var typeEl = assembly.AppendChild(doc.CreateElement("type"));
|
||||
var tattr = doc.CreateAttribute("fullname");
|
||||
tattr.Value = t;
|
||||
if (typeEl.Attributes != null)
|
||||
{
|
||||
typeEl.Attributes.Append(tattr);
|
||||
var pattr = doc.CreateAttribute("preserve");
|
||||
pattr.Value = "nothing";
|
||||
typeEl.Attributes.Append(pattr);
|
||||
var sattr = doc.CreateAttribute("serialized");
|
||||
sattr.Value = "true";
|
||||
typeEl.Attributes.Append(sattr);
|
||||
}
|
||||
}
|
||||
m_SerializedClassesPerAssembly.Remove(assemblyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Add serialize reference classes which are contained in other assemblies not yet removed.
|
||||
foreach (var k in m_SerializedClassesPerAssembly.OrderBy(a => a.Key))
|
||||
{
|
||||
var assembly = linker.AppendChild(doc.CreateElement("assembly"));
|
||||
var attr = doc.CreateAttribute("fullname");
|
||||
attr.Value = k.Key;
|
||||
if (assembly.Attributes != null)
|
||||
{
|
||||
assembly.Attributes.Append(attr);
|
||||
//Add content for this
|
||||
foreach (var t in k.Value.OrderBy(t => t))
|
||||
{
|
||||
var typeEl = assembly.AppendChild(doc.CreateElement("type"));
|
||||
var tattr = doc.CreateAttribute("fullname");
|
||||
tattr.Value = t;
|
||||
if (typeEl.Attributes != null)
|
||||
{
|
||||
typeEl.Attributes.Append(tattr);
|
||||
var pattr = doc.CreateAttribute("preserve");
|
||||
pattr.Value = "nothing";
|
||||
typeEl.Attributes.Append(pattr);
|
||||
var sattr = doc.CreateAttribute("serialized");
|
||||
sattr.Value = "true";
|
||||
typeEl.Attributes.Append(sattr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
doc.Save(path);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c41ecdd192a66174db9ae6b457c3a68c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,227 @@
|
|||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
|
||||
rights reserved.
|
||||
|
||||
License to copy and use this software is granted provided that it
|
||||
is identified as the "RSA Data Security, Inc. MD4 Message-Digest
|
||||
Algorithm" in all material mentioning or referencing this software
|
||||
or this function.
|
||||
|
||||
License is also granted to make and use derivative works provided
|
||||
that such works are identified as "derived from the RSA Data
|
||||
Security, Inc. MD4 Message-Digest Algorithm" in all material
|
||||
mentioning or referencing the derived work.
|
||||
|
||||
RSA Data Security, Inc. makes no representations concerning either
|
||||
the merchantability of this software or the suitability of this
|
||||
software for any particular purpose. It is provided "as is"
|
||||
without express or implied warranty of any kind.
|
||||
|
||||
These notices must be retained in any copies of any part of this
|
||||
documentation and/or software.
|
||||
*/
|
||||
|
||||
/* Converted to C# by Ryan Caltabiano for Unity Technologies */
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of the MD4 hashing algorithm.
|
||||
/// </summary>
|
||||
public sealed class MD4 : HashAlgorithm
|
||||
{
|
||||
uint[] m_Buffer;
|
||||
uint[] m_Block;
|
||||
uint m_Bytes;
|
||||
|
||||
static readonly byte[] kPadding =
|
||||
{
|
||||
0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
};
|
||||
|
||||
MD4()
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new MD4 hashing algorithm object.
|
||||
/// </summary>
|
||||
/// <returns>Returns the new MD4 hashing algorithm instance.</returns>
|
||||
public new static MD4 Create()
|
||||
{
|
||||
return new MD4();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the hash algorithm to its initial state.
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
m_Buffer = new uint[]
|
||||
{
|
||||
0x67452301,
|
||||
0xefcdab89,
|
||||
0x98badcfe,
|
||||
0x10325476
|
||||
};
|
||||
|
||||
m_Block = new uint[16];
|
||||
m_Bytes = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When overridden in a derived class, routes data written to the object into the hash algorithm for computing the hash.
|
||||
/// </summary>
|
||||
/// <param name="array">The input to compute the hash code for.</param>
|
||||
/// <param name="ibStart">The offset into the byte array from which to begin using data.</param>
|
||||
/// <param name="cbSize">The number of bytes in the byte array to use as data.</param>
|
||||
protected override void HashCore(byte[] array, int ibStart, int cbSize)
|
||||
{
|
||||
if (array == null)
|
||||
throw new ArgumentNullException("array");
|
||||
|
||||
for (var i = 0; i < cbSize; i++)
|
||||
{
|
||||
var b = array[ibStart + i];
|
||||
var c = m_Bytes & 63;
|
||||
var k = c >> 2;
|
||||
var s = (c & 3) << 3;
|
||||
m_Block[k] = (m_Block[k] & ~((uint)255 << (int)s)) | ((uint)b << (int)s);
|
||||
|
||||
if (c == 63)
|
||||
ProcessBlock();
|
||||
|
||||
m_Bytes++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When overridden in a derived class, finalizes the hash computation after the last data is processed by the cryptographic hash algorithm.
|
||||
/// </summary>
|
||||
/// <returns>Returns the computed hash.</returns>
|
||||
protected override byte[] HashFinal()
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(m_Bytes << 3);
|
||||
|
||||
var length = ((m_Bytes + 8) & 0x7fffffc0) + 56 - m_Bytes;
|
||||
HashCore(kPadding, 0, (int)length);
|
||||
HashCore(bytes, 0, 4);
|
||||
HashCore(kPadding, kPadding.Length - 4, 4);
|
||||
|
||||
var output = new byte[16];
|
||||
output[0] = (byte)(m_Buffer[0] & 0xff);
|
||||
output[1] = (byte)((m_Buffer[0] >> 8) & 0xff);
|
||||
output[2] = (byte)((m_Buffer[0] >> 16) & 0xff);
|
||||
output[3] = (byte)((m_Buffer[0] >> 24) & 0xff);
|
||||
output[4] = (byte)(m_Buffer[1] & 0xff);
|
||||
output[5] = (byte)((m_Buffer[1] >> 8) & 0xff);
|
||||
output[6] = (byte)((m_Buffer[1] >> 16) & 0xff);
|
||||
output[7] = (byte)((m_Buffer[1] >> 24) & 0xff);
|
||||
output[8] = (byte)(m_Buffer[2] & 0xff);
|
||||
output[9] = (byte)((m_Buffer[2] >> 8) & 0xff);
|
||||
output[10] = (byte)((m_Buffer[2] >> 16) & 0xff);
|
||||
output[11] = (byte)((m_Buffer[2] >> 24) & 0xff);
|
||||
output[12] = (byte)(m_Buffer[3] & 0xff);
|
||||
output[13] = (byte)((m_Buffer[3] >> 8) & 0xff);
|
||||
output[14] = (byte)((m_Buffer[3] >> 16) & 0xff);
|
||||
output[15] = (byte)((m_Buffer[3] >> 24) & 0xff);
|
||||
return output;
|
||||
}
|
||||
|
||||
void ProcessBlock()
|
||||
{
|
||||
var buffer = new uint[4];
|
||||
Array.Copy(m_Buffer, buffer, 4);
|
||||
|
||||
/* Round 1 */
|
||||
buffer[0] = RotateLeft(buffer[0] + F(buffer[1], buffer[2], buffer[3]) + m_Block[0], 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + F(buffer[0], buffer[1], buffer[2]) + m_Block[1], 7);
|
||||
buffer[2] = RotateLeft(buffer[2] + F(buffer[3], buffer[0], buffer[1]) + m_Block[2], 11);
|
||||
buffer[1] = RotateLeft(buffer[1] + F(buffer[2], buffer[3], buffer[0]) + m_Block[3], 19);
|
||||
buffer[0] = RotateLeft(buffer[0] + F(buffer[1], buffer[2], buffer[3]) + m_Block[4], 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + F(buffer[0], buffer[1], buffer[2]) + m_Block[5], 7);
|
||||
buffer[2] = RotateLeft(buffer[2] + F(buffer[3], buffer[0], buffer[1]) + m_Block[6], 11);
|
||||
buffer[1] = RotateLeft(buffer[1] + F(buffer[2], buffer[3], buffer[0]) + m_Block[7], 19);
|
||||
buffer[0] = RotateLeft(buffer[0] + F(buffer[1], buffer[2], buffer[3]) + m_Block[8], 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + F(buffer[0], buffer[1], buffer[2]) + m_Block[9], 7);
|
||||
buffer[2] = RotateLeft(buffer[2] + F(buffer[3], buffer[0], buffer[1]) + m_Block[10], 11);
|
||||
buffer[1] = RotateLeft(buffer[1] + F(buffer[2], buffer[3], buffer[0]) + m_Block[11], 19);
|
||||
buffer[0] = RotateLeft(buffer[0] + F(buffer[1], buffer[2], buffer[3]) + m_Block[12], 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + F(buffer[0], buffer[1], buffer[2]) + m_Block[13], 7);
|
||||
buffer[2] = RotateLeft(buffer[2] + F(buffer[3], buffer[0], buffer[1]) + m_Block[14], 11);
|
||||
buffer[1] = RotateLeft(buffer[1] + F(buffer[2], buffer[3], buffer[0]) + m_Block[15], 19);
|
||||
|
||||
/* Round 2 */
|
||||
buffer[0] = RotateLeft(buffer[0] + G(buffer[1], buffer[2], buffer[3]) + m_Block[0] + 0x5A827999, 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + G(buffer[0], buffer[1], buffer[2]) + m_Block[4] + 0x5A827999, 5);
|
||||
buffer[2] = RotateLeft(buffer[2] + G(buffer[3], buffer[0], buffer[1]) + m_Block[8] + 0x5A827999, 9);
|
||||
buffer[1] = RotateLeft(buffer[1] + G(buffer[2], buffer[3], buffer[0]) + m_Block[12] + 0x5A827999, 13);
|
||||
buffer[0] = RotateLeft(buffer[0] + G(buffer[1], buffer[2], buffer[3]) + m_Block[1] + 0x5A827999, 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + G(buffer[0], buffer[1], buffer[2]) + m_Block[5] + 0x5A827999, 5);
|
||||
buffer[2] = RotateLeft(buffer[2] + G(buffer[3], buffer[0], buffer[1]) + m_Block[9] + 0x5A827999, 9);
|
||||
buffer[1] = RotateLeft(buffer[1] + G(buffer[2], buffer[3], buffer[0]) + m_Block[13] + 0x5A827999, 13);
|
||||
buffer[0] = RotateLeft(buffer[0] + G(buffer[1], buffer[2], buffer[3]) + m_Block[2] + 0x5A827999, 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + G(buffer[0], buffer[1], buffer[2]) + m_Block[6] + 0x5A827999, 5);
|
||||
buffer[2] = RotateLeft(buffer[2] + G(buffer[3], buffer[0], buffer[1]) + m_Block[10] + 0x5A827999, 9);
|
||||
buffer[1] = RotateLeft(buffer[1] + G(buffer[2], buffer[3], buffer[0]) + m_Block[14] + 0x5A827999, 13);
|
||||
buffer[0] = RotateLeft(buffer[0] + G(buffer[1], buffer[2], buffer[3]) + m_Block[3] + 0x5A827999, 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + G(buffer[0], buffer[1], buffer[2]) + m_Block[7] + 0x5A827999, 5);
|
||||
buffer[2] = RotateLeft(buffer[2] + G(buffer[3], buffer[0], buffer[1]) + m_Block[11] + 0x5A827999, 9);
|
||||
buffer[1] = RotateLeft(buffer[1] + G(buffer[2], buffer[3], buffer[0]) + m_Block[15] + 0x5A827999, 13);
|
||||
|
||||
/* Round 3 */
|
||||
buffer[0] = RotateLeft(buffer[0] + H(buffer[1], buffer[2], buffer[3]) + m_Block[0] + 0x6ED9EBA1, 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + H(buffer[0], buffer[1], buffer[2]) + m_Block[8] + 0x6ED9EBA1, 9);
|
||||
buffer[2] = RotateLeft(buffer[2] + H(buffer[3], buffer[0], buffer[1]) + m_Block[4] + 0x6ED9EBA1, 11);
|
||||
buffer[1] = RotateLeft(buffer[1] + H(buffer[2], buffer[3], buffer[0]) + m_Block[12] + 0x6ED9EBA1, 15);
|
||||
buffer[0] = RotateLeft(buffer[0] + H(buffer[1], buffer[2], buffer[3]) + m_Block[2] + 0x6ED9EBA1, 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + H(buffer[0], buffer[1], buffer[2]) + m_Block[10] + 0x6ED9EBA1, 9);
|
||||
buffer[2] = RotateLeft(buffer[2] + H(buffer[3], buffer[0], buffer[1]) + m_Block[6] + 0x6ED9EBA1, 11);
|
||||
buffer[1] = RotateLeft(buffer[1] + H(buffer[2], buffer[3], buffer[0]) + m_Block[14] + 0x6ED9EBA1, 15);
|
||||
buffer[0] = RotateLeft(buffer[0] + H(buffer[1], buffer[2], buffer[3]) + m_Block[1] + 0x6ED9EBA1, 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + H(buffer[0], buffer[1], buffer[2]) + m_Block[9] + 0x6ED9EBA1, 9);
|
||||
buffer[2] = RotateLeft(buffer[2] + H(buffer[3], buffer[0], buffer[1]) + m_Block[5] + 0x6ED9EBA1, 11);
|
||||
buffer[1] = RotateLeft(buffer[1] + H(buffer[2], buffer[3], buffer[0]) + m_Block[13] + 0x6ED9EBA1, 15);
|
||||
buffer[0] = RotateLeft(buffer[0] + H(buffer[1], buffer[2], buffer[3]) + m_Block[3] + 0x6ED9EBA1, 3);
|
||||
buffer[3] = RotateLeft(buffer[3] + H(buffer[0], buffer[1], buffer[2]) + m_Block[11] + 0x6ED9EBA1, 9);
|
||||
buffer[2] = RotateLeft(buffer[2] + H(buffer[3], buffer[0], buffer[1]) + m_Block[7] + 0x6ED9EBA1, 11);
|
||||
buffer[1] = RotateLeft(buffer[1] + H(buffer[2], buffer[3], buffer[0]) + m_Block[15] + 0x6ED9EBA1, 15);
|
||||
|
||||
unchecked
|
||||
{
|
||||
m_Buffer[0] += buffer[0];
|
||||
m_Buffer[1] += buffer[1];
|
||||
m_Buffer[2] += buffer[2];
|
||||
m_Buffer[3] += buffer[3];
|
||||
}
|
||||
}
|
||||
|
||||
static uint F(uint x, uint y, uint z)
|
||||
{
|
||||
// XY v not(X) Z
|
||||
return (x & y) | (~x & z);
|
||||
}
|
||||
|
||||
static uint G(uint x, uint y, uint z)
|
||||
{
|
||||
// XY v XZ v YZ
|
||||
return (x & y) | (x & z) | (y & z);
|
||||
}
|
||||
|
||||
static uint H(uint x, uint y, uint z)
|
||||
{
|
||||
// X XOR Y XOR Z
|
||||
return x ^ y ^ z;
|
||||
}
|
||||
|
||||
static uint RotateLeft(uint x, uint n)
|
||||
{
|
||||
return (x << (int)n) | (x >> (32 - (int)n));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3d01c0faeb539534689fe05bc369751e
|
||||
timeCreated: 1504709533
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,30 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
static class PlayerSettingsApi
|
||||
{
|
||||
static SerializedObject m_Target;
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
static SerializedProperty m_NumberOfMipsStripped;
|
||||
#endif
|
||||
|
||||
static PlayerSettingsApi()
|
||||
{
|
||||
var playerSettings = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/ProjectSettings.asset");
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
m_Target = new SerializedObject(playerSettings);
|
||||
m_NumberOfMipsStripped = m_Target.FindProperty("numberOfMipsStripped");
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
internal static int GetNumberOfMipsStripped()
|
||||
{
|
||||
m_Target.Update();
|
||||
return m_NumberOfMipsStripped.intValue;
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 26d46fb4de7c9fd40b94f75aaf6bff0f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,314 @@
|
|||
using UnityEngine;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor.Build.Pipeline.Tasks;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Static class containing per project build settings.
|
||||
/// </summary>
|
||||
public static class ScriptableBuildPipeline
|
||||
{
|
||||
private class GUIScope : GUI.Scope
|
||||
{
|
||||
float m_LabelWidth;
|
||||
public GUIScope(float layoutMaxWidth)
|
||||
{
|
||||
m_LabelWidth = EditorGUIUtility.labelWidth;
|
||||
EditorGUIUtility.labelWidth = 250;
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Space(10);
|
||||
GUILayout.BeginVertical();
|
||||
GUILayout.Space(15);
|
||||
}
|
||||
|
||||
public GUIScope() : this(500)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void CloseScope()
|
||||
{
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.EndHorizontal();
|
||||
EditorGUIUtility.labelWidth = m_LabelWidth;
|
||||
}
|
||||
}
|
||||
|
||||
internal class Properties
|
||||
{
|
||||
public static readonly GUIContent generalSettings = EditorGUIUtility.TrTextContent("General Settings");
|
||||
public static readonly GUIContent threadedArchiving = EditorGUIUtility.TrTextContent("Threaded Archiving", "Thread the archiving and compress build stage.");
|
||||
public static readonly GUIContent logCacheMiss = EditorGUIUtility.TrTextContent("Log Cache Miss", "Log a warning on build cache misses. Warning will contain which asset and dependency caused the miss.");
|
||||
public static readonly GUIContent slimWriteResults = EditorGUIUtility.TrTextContent("Slim Write Results", "Reduces the caching of WriteResults data down to the bare minimum for improved cache performance.");
|
||||
public static readonly GUIContent v2Hasher = EditorGUIUtility.TrTextContent("Use V2 Hasher", "Use the same hasher as Asset Database V2. This hasher improves build cache performance, but invalidates the existing build cache.");
|
||||
public static readonly GUIContent hashSeed = EditorGUIUtility.TrTextContent("FileID Generator Seed", "Allows you to specify an additional seed to avoid file identifier collisions during build. This changes the layout of all objects in all bundles and we suggest not changing this value after release.");
|
||||
public static readonly GUIContent randSeed = EditorGUIUtility.TrTextContent("Random");
|
||||
public static readonly GUIContent headerSize = EditorGUIUtility.TrTextContent("Prefab Packed Header Size", "Allows you to specify the size of the header for PrefabPacked asset bundles to avoid file identifier collisions during build. This changes the layout of all objects in all bundles and we suggest not changing this value after release.");
|
||||
public static readonly GUIContent maxCacheSize = EditorGUIUtility.TrTextContent("Maximum Cache Size (GB)", "The size of the Build Cache folder will be kept below this maximum value when possible.");
|
||||
public static readonly GUIContent buildCache = EditorGUIUtility.TrTextContent("Build Cache");
|
||||
public static readonly GUIContent purgeCache = EditorGUIUtility.TrTextContent("Purge Cache");
|
||||
public static readonly GUIContent pruneCache = EditorGUIUtility.TrTextContent("Prune Cache");
|
||||
public static readonly GUIContent cacheSizeIs = EditorGUIUtility.TrTextContent("Cache size is");
|
||||
public static readonly GUIContent pleaseWait = EditorGUIUtility.TrTextContent("Please wait...");
|
||||
public static readonly GUIContent cacheServerConfig = EditorGUIUtility.TrTextContent("Cache Server Configuration");
|
||||
public static readonly GUIContent useBuildCacheServer = EditorGUIUtility.TrTextContent("Use Build Cache Server");
|
||||
public static readonly GUIContent cacheServerHost = EditorGUIUtility.TrTextContent("Cache Server Host");
|
||||
public static readonly GUIContent cacheServerPort = EditorGUIUtility.TrTextContent("Cache Server Port");
|
||||
public static bool startedCalculation = false;
|
||||
public static long currentCacheSize = -1;
|
||||
public static readonly GUIContent useDetailedBuildLog = EditorGUIUtility.TrTextContent("Use Detailed Build Log", "Writes detailed event information in the build log.");
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
internal class Settings
|
||||
{
|
||||
public bool useBuildCacheServer = false;
|
||||
public string cacheServerHost = "";
|
||||
public int cacheServerPort = 8126;
|
||||
public bool threadedArchiving = true;
|
||||
public bool logCacheMiss = false;
|
||||
public bool slimWriteResults = true;
|
||||
public int maximumCacheSize = 20;
|
||||
public bool useDetailedBuildLog = false;
|
||||
#if UNITY_2021_1_OR_NEWER
|
||||
public bool useV2Hasher = true;
|
||||
#elif UNITY_2020_1_OR_NEWER
|
||||
public bool useV2Hasher = false;
|
||||
#endif
|
||||
public int fileIDHashSeed = 0;
|
||||
public int prefabPackedHeaderSize = 2;
|
||||
}
|
||||
|
||||
internal static Settings s_Settings = new Settings();
|
||||
|
||||
/// <summary>
|
||||
/// Flag to determine if the Build Cache Server is to be used.
|
||||
/// </summary>
|
||||
public static bool UseBuildCacheServer
|
||||
{
|
||||
get => s_Settings.useBuildCacheServer;
|
||||
set => CompareAndSet(ref s_Settings.useBuildCacheServer, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The host of the Build Cache Server.
|
||||
/// </summary>
|
||||
public static string CacheServerHost
|
||||
{
|
||||
get => s_Settings.cacheServerHost;
|
||||
set => CompareAndSet(ref s_Settings.cacheServerHost, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The port number for the Build Cache Server.
|
||||
/// </summary>
|
||||
public static int CacheServerPort
|
||||
{
|
||||
get => s_Settings.cacheServerPort;
|
||||
set => CompareAndSet(ref s_Settings.cacheServerPort, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread the archiving and compress build stage.
|
||||
/// </summary>
|
||||
public static bool threadedArchiving
|
||||
{
|
||||
get => s_Settings.threadedArchiving;
|
||||
set => CompareAndSet(ref s_Settings.threadedArchiving, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log a warning on build cache misses. Warning will contain which asset and dependency caused the miss.
|
||||
/// </summary>
|
||||
public static bool logCacheMiss
|
||||
{
|
||||
get => s_Settings.logCacheMiss;
|
||||
set => CompareAndSet(ref s_Settings.logCacheMiss, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduces the caching of WriteResults data down to the bare minimum for improved cache performance.
|
||||
/// </summary>
|
||||
public static bool slimWriteResults
|
||||
{
|
||||
get => s_Settings.slimWriteResults;
|
||||
set => CompareAndSet(ref s_Settings.slimWriteResults, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The size of the Build Cache folder will be kept below this maximum value when possible.
|
||||
/// </summary>
|
||||
public static int maximumCacheSize
|
||||
{
|
||||
get => s_Settings.maximumCacheSize;
|
||||
set => CompareAndSet(ref s_Settings.maximumCacheSize, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true to write more detailed event information in the build log. Set to false to only write basic event information.
|
||||
/// </summary>
|
||||
public static bool useDetailedBuildLog
|
||||
{
|
||||
get => s_Settings.useDetailedBuildLog;
|
||||
set => CompareAndSet(ref s_Settings.useDetailedBuildLog, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true to use the same hasher as Asset Database V2. This hasher improves build cache performance, but invalidates the existing build cache.
|
||||
/// </summary>
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
public static bool useV2Hasher
|
||||
{
|
||||
get => s_Settings.useV2Hasher;
|
||||
set => CompareAndSet(ref s_Settings.useV2Hasher, value);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Internal as we don't want to allow setting these via API. We want to ensure they are saved to json, and checked in to the project version control.
|
||||
internal static int fileIDHashSeed
|
||||
{
|
||||
get => s_Settings.fileIDHashSeed;
|
||||
set => CompareAndSet(ref s_Settings.fileIDHashSeed, value);
|
||||
}
|
||||
|
||||
// Internal as we don't want to allow setting these via API. We want to ensure they are saved to json, and checked in to the project version control.
|
||||
internal static int prefabPackedHeaderSize
|
||||
{
|
||||
get => Mathf.Clamp(s_Settings.prefabPackedHeaderSize, 1, 4);
|
||||
set => CompareAndSet(ref s_Settings.prefabPackedHeaderSize, Mathf.Clamp(value, 1, 4));
|
||||
}
|
||||
|
||||
static void CompareAndSet<T>(ref T property, T value)
|
||||
{
|
||||
if (property.Equals(value))
|
||||
return;
|
||||
|
||||
property = value;
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
internal const string kSettingPath = "ProjectSettings/ScriptableBuildPipeline.json";
|
||||
|
||||
internal static void LoadSettings()
|
||||
{
|
||||
// Load new settings from Json
|
||||
if (File.Exists(kSettingPath))
|
||||
{
|
||||
// Existing projects should keep previous defaults that are now settings
|
||||
s_Settings.prefabPackedHeaderSize = 4;
|
||||
|
||||
var json = File.ReadAllText(kSettingPath);
|
||||
EditorJsonUtility.FromJsonOverwrite(json, s_Settings);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SaveSettings()
|
||||
{
|
||||
var json = EditorJsonUtility.ToJson(s_Settings, true);
|
||||
File.WriteAllText(kSettingPath, json);
|
||||
}
|
||||
|
||||
static ScriptableBuildPipeline()
|
||||
{
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
#if UNITY_2019_1_OR_NEWER
|
||||
[SettingsProvider]
|
||||
static SettingsProvider CreateBuildCacheProvider()
|
||||
{
|
||||
var provider = new SettingsProvider("Preferences/Scriptable Build Pipeline", SettingsScope.User, SettingsProvider.GetSearchKeywordsFromGUIContentProperties<Properties>());
|
||||
provider.guiHandler = sarchContext => OnGUI();
|
||||
return provider;
|
||||
}
|
||||
|
||||
#else
|
||||
[PreferenceItem("Scriptable Build Pipeline")]
|
||||
#endif
|
||||
static void OnGUI()
|
||||
{
|
||||
using (new GUIScope())
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
DrawGeneralProperties();
|
||||
GUILayout.Space(15);
|
||||
DrawBuildCacheProperties();
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawGeneralProperties()
|
||||
{
|
||||
GUILayout.Label(Properties.generalSettings, EditorStyles.boldLabel);
|
||||
|
||||
if (ReflectionExtensions.SupportsMultiThreadedArchiving)
|
||||
s_Settings.threadedArchiving = EditorGUILayout.Toggle(Properties.threadedArchiving, s_Settings.threadedArchiving);
|
||||
|
||||
s_Settings.logCacheMiss = EditorGUILayout.Toggle(Properties.logCacheMiss, s_Settings.logCacheMiss);
|
||||
s_Settings.slimWriteResults = EditorGUILayout.Toggle(Properties.slimWriteResults, s_Settings.slimWriteResults);
|
||||
s_Settings.useDetailedBuildLog = EditorGUILayout.Toggle(Properties.useDetailedBuildLog, s_Settings.useDetailedBuildLog);
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
s_Settings.useV2Hasher = EditorGUILayout.Toggle(Properties.v2Hasher, s_Settings.useV2Hasher);
|
||||
#endif
|
||||
GUILayout.BeginHorizontal();
|
||||
s_Settings.fileIDHashSeed = EditorGUILayout.IntField(Properties.hashSeed, s_Settings.fileIDHashSeed);
|
||||
if (GUILayout.Button(Properties.randSeed, GUILayout.Width(120)))
|
||||
s_Settings.fileIDHashSeed = (int)(Random.value * uint.MaxValue);
|
||||
GUILayout.EndHorizontal();
|
||||
s_Settings.prefabPackedHeaderSize = EditorGUILayout.IntSlider(Properties.headerSize, s_Settings.prefabPackedHeaderSize, 1, 4);
|
||||
}
|
||||
|
||||
static void DrawBuildCacheProperties()
|
||||
{
|
||||
GUILayout.Label(Properties.buildCache, EditorStyles.boldLabel);
|
||||
// Show Gigabytes to the user.
|
||||
const int kMinSizeInGigabytes = 1;
|
||||
const int kMaxSizeInGigabytes = 200;
|
||||
|
||||
// Write size in GigaBytes.
|
||||
s_Settings.maximumCacheSize = EditorGUILayout.IntSlider(Properties.maxCacheSize, s_Settings.maximumCacheSize, kMinSizeInGigabytes, kMaxSizeInGigabytes);
|
||||
|
||||
GUILayout.BeginHorizontal(GUILayout.MaxWidth(500));
|
||||
if (GUILayout.Button(Properties.purgeCache, GUILayout.Width(120)))
|
||||
{
|
||||
BuildCache.PurgeCache(true);
|
||||
Properties.startedCalculation = false;
|
||||
}
|
||||
|
||||
if (GUILayout.Button(Properties.pruneCache, GUILayout.Width(120)))
|
||||
{
|
||||
BuildCache.PruneCache();
|
||||
Properties.startedCalculation = false;
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
// Current cache size
|
||||
if (!Properties.startedCalculation)
|
||||
{
|
||||
Properties.startedCalculation = true;
|
||||
ThreadPool.QueueUserWorkItem((state) =>
|
||||
{
|
||||
BuildCache.ComputeCacheSizeAndFolders(out Properties.currentCacheSize, out List<BuildCache.CacheFolder> cacheFolders);
|
||||
});
|
||||
}
|
||||
|
||||
if (Properties.currentCacheSize >= 0)
|
||||
GUILayout.Label(Properties.cacheSizeIs.text + " " + EditorUtility.FormatBytes(Properties.currentCacheSize));
|
||||
else
|
||||
GUILayout.Label(Properties.cacheSizeIs.text + " is being calculated...");
|
||||
|
||||
GUILayout.Space(15);
|
||||
GUILayout.Label(Properties.cacheServerConfig, EditorStyles.boldLabel);
|
||||
|
||||
s_Settings.useBuildCacheServer = EditorGUILayout.Toggle(Properties.useBuildCacheServer, s_Settings.useBuildCacheServer);
|
||||
if (s_Settings.useBuildCacheServer)
|
||||
{
|
||||
s_Settings.cacheServerHost = EditorGUILayout.TextField(Properties.cacheServerHost, s_Settings.cacheServerHost);
|
||||
s_Settings.cacheServerPort = EditorGUILayout.IntField(Properties.cacheServerPort, s_Settings.cacheServerPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e14d3e528f112e445aa46b7870f93944
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs information about the progress tracker.
|
||||
/// </summary>
|
||||
public class ProgressLoggingTracker : ProgressTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new progress tracking object.
|
||||
/// </summary>
|
||||
public ProgressLoggingTracker()
|
||||
{
|
||||
BuildLogger.Log(string.Format("[{0}] Progress Tracker Started.", DateTime.Now.ToString()));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool UpdateTask(string taskTitle)
|
||||
{
|
||||
BuildLogger.Log(string.Format("[{0}] {1:P2} Running Task: '{2}'", DateTime.Now.ToString(), Progress.ToString(), taskTitle));
|
||||
return base.UpdateTask(taskTitle);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool UpdateInfo(string taskInfo)
|
||||
{
|
||||
BuildLogger.Log(string.Format("[{0}] {1:P2} Running Task: '{2}' Information: '{3}'", DateTime.Now.ToString(), Progress.ToString(), CurrentTaskTitle, taskInfo));
|
||||
return base.UpdateInfo(taskInfo);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
BuildLogger.Log(string.Format("[{0}] Progress Tracker Completed.", DateTime.Now.ToString()));
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fdaf4f8f6845474a8b19554240f1e8ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,117 @@
|
|||
using System;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps track of the SBP build progress.
|
||||
/// </summary>
|
||||
public class ProgressTracker : IProgressTracker, IDisposable
|
||||
{
|
||||
const long k_TicksPerSecond = 10000000;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the number of tasks
|
||||
/// </summary>
|
||||
public int TaskCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the amount of progress done as a decimal.
|
||||
/// </summary>
|
||||
public float Progress { get { return CurrentTask / (float)TaskCount; } }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the amount of updates per second.
|
||||
/// </summary>
|
||||
public uint UpdatesPerSecond
|
||||
{
|
||||
get { return (uint)(k_TicksPerSecond / UpdateFrequency); }
|
||||
set { UpdateFrequency = k_TicksPerSecond / Math.Max(value, 1); }
|
||||
}
|
||||
|
||||
bool m_Disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the id of currently running task.
|
||||
/// </summary>
|
||||
protected int CurrentTask { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the name of the currently running task.
|
||||
/// </summary>
|
||||
protected string CurrentTaskTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores current the time stamp.
|
||||
/// </summary>
|
||||
protected long TimeStamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the task update frequency.
|
||||
/// </summary>
|
||||
protected long UpdateFrequency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores information about the current task.
|
||||
/// </summary>
|
||||
public ProgressTracker()
|
||||
{
|
||||
CurrentTask = 0;
|
||||
CurrentTaskTitle = "";
|
||||
TimeStamp = 0;
|
||||
UpdateFrequency = k_TicksPerSecond / 100;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the progress bar to reflect the new running task.
|
||||
/// </summary>
|
||||
/// <param name="taskTitle">The name of the new task.</param>
|
||||
/// <returns>Returns true if the progress bar is running. Returns false if the user cancels the progress bar.</returns>
|
||||
public virtual bool UpdateTask(string taskTitle)
|
||||
{
|
||||
CurrentTask++;
|
||||
CurrentTaskTitle = taskTitle;
|
||||
TimeStamp = 0;
|
||||
return !EditorUtility.DisplayCancelableProgressBar(CurrentTaskTitle, "", Progress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the information displayed for currently running task.
|
||||
/// </summary>
|
||||
/// <param name="taskInfo">The task information.</param>
|
||||
/// <returns>Returns true if the progress bar is running. Returns false if the user cancels the progress bar.</returns>
|
||||
public virtual bool UpdateInfo(string taskInfo)
|
||||
{
|
||||
var currentTicks = DateTime.Now.Ticks;
|
||||
if (currentTicks - TimeStamp < UpdateFrequency)
|
||||
return true;
|
||||
|
||||
TimeStamp = currentTicks;
|
||||
return !EditorUtility.DisplayCancelableProgressBar(CurrentTaskTitle, taskInfo, Progress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of the progress tracker instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of the progress tracker instance and clears the popup progress bar.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Set to true to clear the popup progress bar. Set to false to leave the progress bar as is.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (m_Disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
EditorUtility.ClearProgressBar();
|
||||
|
||||
m_Disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 02d56b860c3f05a47a85e63f62578654
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,31 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
static class QualitySettingsApi
|
||||
{
|
||||
static SerializedObject m_Target;
|
||||
static SerializedProperty m_QualitySettingsProperty;
|
||||
|
||||
static QualitySettingsApi()
|
||||
{
|
||||
var qualitySettings = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/QualitySettings.asset");
|
||||
m_Target = new SerializedObject(qualitySettings);
|
||||
m_QualitySettingsProperty = m_Target.FindProperty("m_QualitySettings");
|
||||
}
|
||||
|
||||
internal static int GetNumberOfLODsStripped()
|
||||
{
|
||||
m_Target.Update();
|
||||
int strippedLODs = int.MaxValue;
|
||||
int count = m_QualitySettingsProperty.arraySize;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var element = m_QualitySettingsProperty.GetArrayElementAtIndex(i);
|
||||
var maximumLODLevel = element.FindPropertyRelative("maximumLODLevel");
|
||||
strippedLODs = Mathf.Min(strippedLODs, maximumLODLevel.intValue);
|
||||
}
|
||||
return strippedLODs == int.MaxValue ? 0 : strippedLODs;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dad58be2203967e4f927f5193b96266a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,91 @@
|
|||
#if !UNITY_2019_3_OR_NEWER
|
||||
#define NOT_UNITY_2019_3_OR_NEWER
|
||||
#endif
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor.Build.Content;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
static class ReflectionExtensions
|
||||
{
|
||||
static FieldInfo WriteResult_SerializedObjects;
|
||||
static FieldInfo WriteResult_ResourceFiles;
|
||||
|
||||
static FieldInfo SceneDependencyInfo_Scene;
|
||||
static FieldInfo SceneDependencyInfo_ProcessedScene;
|
||||
static FieldInfo SceneDependencyInfo_ReferencedObjects;
|
||||
|
||||
static bool BuildUsageTagSet_SupportsFilterToSubset;
|
||||
static bool ContentBuildInterface_SupportsMultiThreadedArchiving;
|
||||
|
||||
static ReflectionExtensions()
|
||||
{
|
||||
WriteResult_SerializedObjects = typeof(WriteResult).GetField("m_SerializedObjects", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
WriteResult_ResourceFiles = typeof(WriteResult).GetField("m_ResourceFiles", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
SceneDependencyInfo_Scene = typeof(SceneDependencyInfo).GetField("m_Scene", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
SceneDependencyInfo_ProcessedScene = typeof(SceneDependencyInfo).GetField("m_ProcessedScene", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
SceneDependencyInfo_ReferencedObjects = typeof(SceneDependencyInfo).GetField("m_ReferencedObjects", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
BuildUsageTagSet_SupportsFilterToSubset = typeof(BuildUsageTagSet).GetMethod("FilterToSubset") != null;
|
||||
foreach (MethodInfo info in typeof(ContentBuildInterface).GetMethods().Where(x => x.Name == "ArchiveAndCompress"))
|
||||
{
|
||||
foreach (var attr in info.CustomAttributes)
|
||||
{
|
||||
ContentBuildInterface_SupportsMultiThreadedArchiving = attr.AttributeType.Name == "ThreadSafeAttribute";
|
||||
if (ContentBuildInterface_SupportsMultiThreadedArchiving)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool SupportsFilterToSubset => BuildUsageTagSet_SupportsFilterToSubset;
|
||||
|
||||
public static bool SupportsMultiThreadedArchiving => ContentBuildInterface_SupportsMultiThreadedArchiving;
|
||||
|
||||
public static void SetSerializedObjects(this ref WriteResult result, ObjectSerializedInfo[] osis)
|
||||
{
|
||||
object boxed = result;
|
||||
WriteResult_SerializedObjects.SetValue(boxed, osis);
|
||||
result = (WriteResult)boxed;
|
||||
}
|
||||
|
||||
public static void SetResourceFiles(this ref WriteResult result, ResourceFile[] files)
|
||||
{
|
||||
object boxed = result;
|
||||
WriteResult_ResourceFiles.SetValue(boxed, files);
|
||||
result = (WriteResult)boxed;
|
||||
}
|
||||
|
||||
public static void SetScene(this ref SceneDependencyInfo dependencyInfo, string scene)
|
||||
{
|
||||
object boxed = dependencyInfo;
|
||||
SceneDependencyInfo_Scene.SetValue(boxed, scene);
|
||||
dependencyInfo = (SceneDependencyInfo)boxed;
|
||||
}
|
||||
|
||||
// Use conditionals to remove api from callsite
|
||||
[Conditional("NOT_UNITY_2019_3_OR_NEWER")]
|
||||
public static void SetProcessedScene(this ref SceneDependencyInfo dependencyInfo, string processedScene)
|
||||
{
|
||||
object boxed = dependencyInfo;
|
||||
SceneDependencyInfo_ProcessedScene.SetValue(boxed, processedScene);
|
||||
dependencyInfo = (SceneDependencyInfo)boxed;
|
||||
}
|
||||
|
||||
public static void SetReferencedObjects(this ref SceneDependencyInfo dependencyInfo, ObjectIdentifier[] references)
|
||||
{
|
||||
object boxed = dependencyInfo;
|
||||
SceneDependencyInfo_ReferencedObjects.SetValue(boxed, references);
|
||||
dependencyInfo = (SceneDependencyInfo)boxed;
|
||||
}
|
||||
|
||||
// Extension methods are second to explicit methods, no need to define this out, it is being used as an API contract only
|
||||
public static void FilterToSubset(this BuildUsageTagSet usageSet, ObjectIdentifier[] objectIds)
|
||||
{
|
||||
throw new System.Exception("FilterToSubset is not supported in this Unity version");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a79b2afb1cd133b47b8cf25ace7ad051
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using UnityEditor.Build.Pipeline.Utilities;
|
||||
using UnityEditor.SceneManagement;
|
||||
using System.Linq;
|
||||
|
||||
namespace UnityEditor.Build.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Cleans up scenes in the scene manager.
|
||||
/// </summary>
|
||||
public class SceneStateCleanup : IDisposable
|
||||
{
|
||||
SceneSetup[] m_Scenes;
|
||||
|
||||
bool m_Disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new scene state cleanup object.
|
||||
/// </summary>
|
||||
public SceneStateCleanup()
|
||||
{
|
||||
m_Scenes = EditorSceneManager.GetSceneManagerSetup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of the scene state cleanup instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of the scene state cleanup instance.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Set to true to reset the scenes list. Set to false to leave the scenes list as is.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (m_Disposed)
|
||||
return;
|
||||
|
||||
m_Disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Test runner injects scenes, so we strip those here
|
||||
var scenes = m_Scenes.Where(s => !string.IsNullOrEmpty(s.path)).ToArray();
|
||||
if (!scenes.IsNullOrEmpty())
|
||||
EditorSceneManager.RestoreSceneManagerSetup(scenes);
|
||||
else
|
||||
EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9ba75fbd280d0c849b9b6a53db5056ac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,50 @@
|
|||
#if UNITY_2019_3_OR_NEWER
|
||||
using System.Security.Cryptography;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
public unsafe sealed class SpookyHash : HashAlgorithm
|
||||
{
|
||||
Hash128 m_Hash;
|
||||
|
||||
SpookyHash()
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public new static SpookyHash Create()
|
||||
{
|
||||
return new SpookyHash();
|
||||
}
|
||||
|
||||
public override void Initialize() {}
|
||||
|
||||
protected override void HashCore(byte[] inputBuffer, int inputOffset, int inputCount)
|
||||
{
|
||||
if (inputBuffer == null || inputOffset < 0 || inputCount <= 0 || (inputCount > inputBuffer.Length) || (inputBuffer.Length - inputCount) < inputOffset)
|
||||
return;
|
||||
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
m_Hash.Append(inputBuffer, inputOffset, inputCount);
|
||||
#else
|
||||
throw new System.InvalidOperationException("SpookyHash implementation was unstable and not deterministic prior to Unity 2020.1. Use MD5 or MD4 instead.");
|
||||
#endif
|
||||
}
|
||||
|
||||
protected override byte[] HashFinal()
|
||||
{
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
byte[] results = new byte[UnsafeUtility.SizeOf<Hash128>()];
|
||||
byte* hashPtr = (byte*)UnsafeUtility.AddressOf(ref m_Hash);
|
||||
fixed(byte* d = results)
|
||||
UnsafeUtility.MemCpy(d, hashPtr, results.Length);
|
||||
return results;
|
||||
#else
|
||||
throw new System.InvalidOperationException("SpookyHash implementation was unstable and not deterministic prior to Unity 2020.1. Use MD5 or MD4 instead.");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b84b1a09f476ad84db74f048d23fb8e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,126 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
internal static class TaskCachingUtility
|
||||
{
|
||||
public class WorkItem<T>
|
||||
{
|
||||
public T Context;
|
||||
public int Index;
|
||||
public CacheEntry entry;
|
||||
public string StatusText;
|
||||
public WorkItem(T context, string statusText = "")
|
||||
{
|
||||
this.Context = context;
|
||||
this.StatusText = statusText;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRunCachedCallbacks<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a cache entry for the specified work item.
|
||||
/// </summary>
|
||||
/// <param name="item">The work item.</param>
|
||||
/// <returns>Returns the created entry.</returns>
|
||||
CacheEntry CreateCacheEntry(WorkItem<T> item);
|
||||
|
||||
/// <summary>
|
||||
/// Process the uncached work item.
|
||||
/// </summary>
|
||||
/// <param name="item">The work item.</param>
|
||||
void ProcessUncached(WorkItem<T> item);
|
||||
|
||||
/// <summary>
|
||||
/// Process the cached work item.
|
||||
/// </summary>
|
||||
/// <param name="item">The work item.</param>
|
||||
/// <param name="info">The cached information for the work item.</param>
|
||||
void ProcessCached(WorkItem<T> item, CachedInfo info);
|
||||
|
||||
/// <summary>
|
||||
/// Post processes the work item.
|
||||
/// </summary>
|
||||
/// <param name="item">The work item.</param>
|
||||
void PostProcess(WorkItem<T> item);
|
||||
|
||||
/// <summary>
|
||||
/// Creates cached information for the specified work item.
|
||||
/// </summary>
|
||||
/// <param name="item">The work item.</param>
|
||||
/// <returns>Returns the cached information created.</returns>
|
||||
CachedInfo CreateCachedInfo(WorkItem<T> item);
|
||||
}
|
||||
|
||||
public static ReturnCode RunCachedOperation<T>(IBuildCache cache, IBuildLogger log, IProgressTracker tracker, List<WorkItem<T>> workItems,
|
||||
IRunCachedCallbacks<T> cbs
|
||||
)
|
||||
{
|
||||
using (log.ScopedStep(LogLevel.Info, "RunCachedOperation"))
|
||||
{
|
||||
List<CacheEntry> cacheEntries = null;
|
||||
List<WorkItem<T>> nonCachedItems = workItems;
|
||||
var cachedItems = new List<WorkItem<T>>();
|
||||
|
||||
for (int i = 0; i < workItems.Count; i++)
|
||||
{
|
||||
workItems[i].Index = i;
|
||||
}
|
||||
|
||||
IList<CachedInfo> cachedInfo = null;
|
||||
|
||||
if (cache != null)
|
||||
{
|
||||
using (log.ScopedStep(LogLevel.Info, "Creating Cache Entries"))
|
||||
for (int i = 0; i < workItems.Count; i++)
|
||||
{
|
||||
workItems[i].entry = cbs.CreateCacheEntry(workItems[i]);
|
||||
}
|
||||
|
||||
cacheEntries = workItems.Select(i => i.entry).ToList();
|
||||
|
||||
using (log.ScopedStep(LogLevel.Info, "Load Cached Data"))
|
||||
cache.LoadCachedData(cacheEntries, out cachedInfo);
|
||||
|
||||
cachedItems = workItems.Where(x => cachedInfo[x.Index] != null).ToList();
|
||||
nonCachedItems = workItems.Where(x => cachedInfo[x.Index] == null).ToList();
|
||||
}
|
||||
|
||||
using (log.ScopedStep(LogLevel.Info, "Process Entries"))
|
||||
foreach (WorkItem<T> item in nonCachedItems)
|
||||
{
|
||||
if (!tracker.UpdateInfoUnchecked(item.StatusText))
|
||||
return ReturnCode.Canceled;
|
||||
cbs.ProcessUncached(item);
|
||||
}
|
||||
|
||||
using (log.ScopedStep(LogLevel.Info, "Process Cached Entries"))
|
||||
foreach (WorkItem<T> item in cachedItems)
|
||||
cbs.ProcessCached(item, cachedInfo[item.Index]);
|
||||
|
||||
foreach (WorkItem<T> item in workItems)
|
||||
cbs.PostProcess(item);
|
||||
|
||||
if (cache != null)
|
||||
{
|
||||
List<CachedInfo> uncachedInfo;
|
||||
using (log.ScopedStep(LogLevel.Info, "Saving to Cache"))
|
||||
{
|
||||
using (log.ScopedStep(LogLevel.Info, "Creating Cached Infos"))
|
||||
uncachedInfo = nonCachedItems.Select((item) => cbs.CreateCachedInfo(item)).ToList();
|
||||
cache.SaveCachedData(uncachedInfo);
|
||||
}
|
||||
}
|
||||
|
||||
log.AddEntrySafe(LogLevel.Info, $"Total Entries: {workItems.Count}, Processed: {nonCachedItems.Count}, Cached: {cachedItems.Count}");
|
||||
return ReturnCode.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 44dfdb5bbfab6614a8481d1d899d884b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
|
||||
static class ThreadingManager
|
||||
{
|
||||
public enum ThreadQueues
|
||||
{
|
||||
SaveQueue,
|
||||
UploadQueue,
|
||||
PruneQueue,
|
||||
TotalQueues
|
||||
}
|
||||
|
||||
static Task[] m_Tasks = new Task[(int)ThreadQueues.TotalQueues];
|
||||
|
||||
static ThreadingManager()
|
||||
{
|
||||
EditorApplication.quitting += WaitForOutstandingTasks;
|
||||
AssemblyReloadEvents.beforeAssemblyReload += WaitForOutstandingTasks;
|
||||
}
|
||||
|
||||
internal static void WaitForOutstandingTasks()
|
||||
{
|
||||
var tasks = m_Tasks.Where(x => x != null).ToArray();
|
||||
m_Tasks = new Task[(int)ThreadQueues.TotalQueues];
|
||||
if (tasks.Length > 0)
|
||||
Task.WaitAll(tasks);
|
||||
}
|
||||
|
||||
internal static void QueueTask(ThreadQueues queue, Action<object> action, object state)
|
||||
{
|
||||
var task = m_Tasks[(int)queue];
|
||||
if (queue == ThreadQueues.PruneQueue)
|
||||
{
|
||||
// Prune tasks need to run after any existing queued tasks
|
||||
var tasks = m_Tasks.Where(x => x != null).ToArray();
|
||||
m_Tasks = new Task[(int)ThreadQueues.TotalQueues];
|
||||
if (tasks.Length > 0)
|
||||
task = Task.WhenAll(tasks).ContinueWith(delegate { action.Invoke(state); });
|
||||
else
|
||||
task = Task.Factory.StartNew(action, state);
|
||||
}
|
||||
else if (task == null)
|
||||
{
|
||||
// New Upload or Save tasks need to be done after any queued prune tasks
|
||||
var pruneTask = m_Tasks[(int)ThreadQueues.PruneQueue];
|
||||
if (pruneTask != null)
|
||||
task = pruneTask.ContinueWith(delegate { action.Invoke(state); });
|
||||
else
|
||||
task = Task.Factory.StartNew(action, state);
|
||||
}
|
||||
else
|
||||
task = task.ContinueWith(delegate { action.Invoke(state); });
|
||||
m_Tasks[(int)queue] = task;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8eba8fcb4416d1943b33efb3d9bd01c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,23 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using UnityEditor.Build.Pipeline.Interfaces;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
internal static class TrackerExtensions
|
||||
{
|
||||
public static string HumanReadable(this string camelCased)
|
||||
{
|
||||
return Regex.Replace(camelCased, @"(\B[A-Z]+?(?=[A-Z][^A-Z])|\B[A-Z]+?(?=[^A-Z]))", " $1");
|
||||
}
|
||||
|
||||
public static bool UpdateTaskUnchecked(this IProgressTracker tracker, string taskTitle)
|
||||
{
|
||||
return tracker == null || tracker.UpdateTask(taskTitle);
|
||||
}
|
||||
|
||||
public static bool UpdateInfoUnchecked(this IProgressTracker tracker, string taskInfo)
|
||||
{
|
||||
return tracker == null || tracker.UpdateInfo(taskInfo);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cc721af777e8101459b9a2ea31544b01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e94656a08861c174c9f786affcfd15a7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,660 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities.USerialize
|
||||
{
|
||||
/*
|
||||
* Main USerialize deserialzation class. Used to read instances of types from a stream
|
||||
*
|
||||
* To DeSerialize an object from a stream use code such as
|
||||
*
|
||||
* USerialize.DeSerializer deserializer = new USerialize.DeSerializer();
|
||||
* CachedInfo info = deserializer.DeSerialize<CachedInfo>(new MemoryStream(File.ReadAllBytes(filepath), false));
|
||||
*
|
||||
*/
|
||||
internal class DeSerializer
|
||||
{
|
||||
// Flag that can be set to true prior to deserialization to cause a detailed log of the data read to be emitted to the console. Will slow down deserialization considerably
|
||||
internal bool EmitVerboseLog = false;
|
||||
|
||||
// Cached data about a field in a type we've encountered
|
||||
internal class FieldData
|
||||
{
|
||||
internal FieldInfo m_FieldInfo;
|
||||
internal object m_Setter;
|
||||
internal object m_Getter;
|
||||
}
|
||||
|
||||
// Cached data about a type we've encountered
|
||||
internal class TypeData
|
||||
{
|
||||
// Data for each of the fields in this type, keyed by field name
|
||||
internal Dictionary<string, FieldData> m_Fields;
|
||||
|
||||
// LUT that maps field indices in the serialized data to fields in the object in memory. Reset every time a new object is deserialized in case they were created at different times and the data has changed
|
||||
internal FieldData[] m_FieldIndex;
|
||||
|
||||
// Type of this type
|
||||
internal Type m_Type;
|
||||
|
||||
// Custom delegate we've been given by the client code to create an instance of this type. It is much faster to have the client code create the instances directly than to use the generic Activator system
|
||||
internal ObjectFactory m_ObjectFactory;
|
||||
}
|
||||
|
||||
// Delegate type the client can supply to create instances of a specific type. It is much faster to have the client code create the instances directly than to use the generic Activator system
|
||||
internal delegate object ObjectFactory();
|
||||
|
||||
// Custom deserializers the client code has supplied to deserialize specific types. Usually used for types that the standard reflection based serialization cannot cope with. Keyed by Type
|
||||
Dictionary<Type, ICustomSerializer> m_CustomSerializers = new Dictionary<Type, ICustomSerializer>();
|
||||
|
||||
// Delegates supplied by the client to create instances of specific types, keyed by type name. It is much faster to have the client code create the instances directly than to use the generic Activator system.
|
||||
Dictionary<Type, ObjectFactory> m_ObjectFactories = new Dictionary<Type, ObjectFactory>();
|
||||
|
||||
// Cache of data about types we have encountered, provides better performance than querying the slow reflection API repeatedly
|
||||
Dictionary<string, TypeData> m_TypeDataCache = new Dictionary<string, TypeData>();
|
||||
|
||||
// Version of the object that was deserialized. This is the value supplied by the client code when Serializer.Serialize() was originally called
|
||||
int m_ObjectVersion;
|
||||
internal int ObjectVersion { get { return m_ObjectVersion; } }
|
||||
|
||||
// Reader we are using to read bytes from the stream
|
||||
BinaryReader m_Reader;
|
||||
|
||||
// Version of the serialization format itself read from the stream. Exists to provide data upgrade potential in the future
|
||||
byte m_SerializationVersion;
|
||||
|
||||
// The type/field name stringtable read from the stream
|
||||
string[] m_TypeStringTable;
|
||||
long m_TypeStringTableBytePos;
|
||||
|
||||
// The data value stringtable read from the stream
|
||||
string[] m_DataStringTable;
|
||||
long m_DataStringTableBytePos;
|
||||
|
||||
// LUT to get TypeData from a type name index for this object. Maps type name indices in the serialized data to TypeData entries in m_TypeDataCache (which persists between objects)
|
||||
TypeData[] m_TypeDataByTypeNameIndex;
|
||||
|
||||
// LUT to get a Type instance from a type name index for this object.
|
||||
Type[] m_TypeByTypeNameIndex;
|
||||
|
||||
internal DeSerializer()
|
||||
{
|
||||
}
|
||||
|
||||
internal DeSerializer(ICustomSerializer[] customSerializers, (Type, ObjectFactory)[] objectFactories)
|
||||
{
|
||||
if (customSerializers != null)
|
||||
Array.ForEach(customSerializers, (customSerializer) => AddCustomSerializer(customSerializer));
|
||||
|
||||
if (objectFactories != null)
|
||||
Array.ForEach(objectFactories, (item) => AddObjectFactory(item.Item1, item.Item2));
|
||||
}
|
||||
|
||||
string[] ReadStringTable(long streamPosition)
|
||||
{
|
||||
m_Reader.BaseStream.Position = streamPosition;
|
||||
|
||||
int numStrings = m_Reader.ReadInt32();
|
||||
string[] stringTable = new string[numStrings];
|
||||
for (int stringNum = 0; stringNum < numStrings; stringNum++)
|
||||
{
|
||||
stringTable[stringNum] = m_Reader.ReadString();
|
||||
}
|
||||
return stringTable;
|
||||
}
|
||||
|
||||
void ReadStringTables()
|
||||
{
|
||||
m_TypeStringTableBytePos = m_Reader.ReadInt64();
|
||||
m_DataStringTableBytePos = m_Reader.ReadInt64();
|
||||
|
||||
long dataStartPos = m_Reader.BaseStream.Position;
|
||||
|
||||
m_TypeStringTable = ReadStringTable(m_TypeStringTableBytePos);
|
||||
m_DataStringTable = ReadStringTable(m_DataStringTableBytePos);
|
||||
|
||||
m_Reader.BaseStream.Position = dataStartPos;
|
||||
}
|
||||
|
||||
// Clear data that we cache about types and object contents that can change between objects.
|
||||
void ClearPerObjectCachedData()
|
||||
{
|
||||
// Clear the field index -> field data LUT as this can change between objects
|
||||
foreach (KeyValuePair<string, TypeData> typeDataCacheEntry in m_TypeDataCache)
|
||||
{
|
||||
typeDataCacheEntry.Value.m_FieldIndex = null;
|
||||
}
|
||||
|
||||
// Clear the type name index -> type data LUT as this can change between objects
|
||||
m_TypeDataByTypeNameIndex = new TypeData[m_TypeStringTable.Length];
|
||||
m_TypeByTypeNameIndex = new Type[m_TypeStringTable.Length];
|
||||
}
|
||||
|
||||
// Main deserialize function. Creates and reads an instance of the specified type from the stream
|
||||
internal ClassType DeSerialize<ClassType>(Stream stream) where ClassType : new()
|
||||
{
|
||||
m_Reader = new BinaryReader(stream);
|
||||
|
||||
m_SerializationVersion = m_Reader.ReadByte();
|
||||
if (m_SerializationVersion != Serializer.SerializationVersion)
|
||||
throw new InvalidDataException($"Data stream is using an incompatible serialization format. Stream is version {m_SerializationVersion} but code requires version {Serializer.SerializationVersion}. The stream should be re-created with the current code");
|
||||
|
||||
m_ObjectVersion = m_Reader.ReadInt32();
|
||||
|
||||
ReadStringTables();
|
||||
|
||||
ClearPerObjectCachedData();
|
||||
|
||||
ClassType instance = (ClassType)ReadObject(0);
|
||||
|
||||
if (m_Reader.BaseStream.Position != m_TypeStringTableBytePos)
|
||||
throw new InvalidDataException($"Did not read entire stream in DeSerialize. Read to +{m_Reader.BaseStream.Position} but expected +{m_TypeStringTableBytePos}");
|
||||
|
||||
// NOTE: The reader is deliberately not disposed here as doing so would also close the stream but we rely on the outer code to manage the lifetime of the stream
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Call to start readingdirectly from a stream, used primarily for testing USerialize functions in isolation
|
||||
internal void StartReadingFromStream(Stream stream)
|
||||
{
|
||||
m_Reader = new BinaryReader(stream);
|
||||
}
|
||||
|
||||
// Return the object version from a serialized stream without doing anything else, this is the object version passed in the original Serializer.Serialize() call that created the stream.
|
||||
// Resets the stream read position back to where it was on entry before returning
|
||||
internal int GetObjectVersion(Stream stream)
|
||||
{
|
||||
long startPos = m_Reader.BaseStream.Position;
|
||||
|
||||
m_Reader.ReadByte(); // Serialization version
|
||||
int objectVersion = m_Reader.ReadInt32();
|
||||
|
||||
m_Reader.BaseStream.Position = startPos;
|
||||
|
||||
return objectVersion;
|
||||
}
|
||||
|
||||
// Add a custom deserializer function to handle a specific type
|
||||
internal void AddCustomSerializer(ICustomSerializer customSerializer)
|
||||
{
|
||||
m_CustomSerializers.Add(customSerializer.GetType(), customSerializer);
|
||||
}
|
||||
|
||||
// Add an object factory function that can create instances of a named type
|
||||
internal void AddObjectFactory(Type type, ObjectFactory objectFactory)
|
||||
{
|
||||
m_ObjectFactories.Add(type, objectFactory);
|
||||
}
|
||||
|
||||
// Get the type data for the specified type. If it exists in the cache it is returned directly, otherwise the type data is gathered and stored in the cache before being returned
|
||||
TypeData GetTypeData(string assemblyQualifiedTypeName)
|
||||
{
|
||||
if (!m_TypeDataCache.TryGetValue(assemblyQualifiedTypeName, out TypeData typeData))
|
||||
{
|
||||
typeData = new TypeData();
|
||||
typeData.m_Fields = new Dictionary<string, FieldData>();
|
||||
typeData.m_Type = Type.GetType(assemblyQualifiedTypeName);
|
||||
if (typeData.m_Type == null)
|
||||
throw new InvalidDataException($"Could not create type for '{assemblyQualifiedTypeName}'");
|
||||
|
||||
m_ObjectFactories.TryGetValue(typeData.m_Type, out typeData.m_ObjectFactory);
|
||||
|
||||
FieldInfo[] fieldArray = typeData.m_Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
for (int fieldNum = 0; fieldNum < fieldArray.Length; fieldNum++)
|
||||
{
|
||||
FieldData fieldData = new FieldData();
|
||||
fieldData.m_FieldInfo = fieldArray[fieldNum];
|
||||
|
||||
if (fieldData.m_FieldInfo.FieldType == typeof(int))
|
||||
fieldData.m_Setter = CreateSetter<int>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(uint))
|
||||
fieldData.m_Setter = CreateSetter<uint>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(long))
|
||||
fieldData.m_Setter = CreateSetter<long>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(ulong))
|
||||
fieldData.m_Setter = CreateSetter<ulong>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(byte))
|
||||
fieldData.m_Setter = CreateSetter<byte>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(bool))
|
||||
fieldData.m_Setter = CreateSetter<bool>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(string))
|
||||
fieldData.m_Setter = CreateSetter<string>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(GUID))
|
||||
fieldData.m_Setter = CreateSetter<GUID>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(Hash128))
|
||||
fieldData.m_Setter = CreateSetter<Hash128>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(Type))
|
||||
fieldData.m_Setter = CreateSetter<Type>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(byte[]))
|
||||
{
|
||||
fieldData.m_Setter = CreateSetter<byte[]>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
fieldData.m_Getter = CreateObjectGetter(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
}
|
||||
// Per customer request
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(ulong[]))
|
||||
{
|
||||
fieldData.m_Setter = CreateSetter<ulong[]>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
fieldData.m_Getter = CreateObjectGetter(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
}
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(string[]))
|
||||
{
|
||||
fieldData.m_Setter = CreateSetter<string[]>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
fieldData.m_Getter = CreateObjectGetter(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
}
|
||||
else if (fieldData.m_FieldInfo.FieldType == typeof(Type[]))
|
||||
{
|
||||
fieldData.m_Setter = CreateSetter<Type[]>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
fieldData.m_Getter = CreateObjectGetter(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
}
|
||||
else if (fieldData.m_FieldInfo.FieldType.IsEnum)
|
||||
fieldData.m_Setter = CreateObjectSetter<int>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (fieldData.m_FieldInfo.FieldType.IsValueType && (!fieldData.m_FieldInfo.FieldType.IsPrimitive))
|
||||
fieldData.m_Setter = CreateObjectSetter<object>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
else if (typeof(Array).IsAssignableFrom(fieldData.m_FieldInfo.FieldType))
|
||||
{
|
||||
fieldData.m_Setter = CreateObjectSetter<object>(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
fieldData.m_Getter = CreateObjectGetter(typeData.m_Type, fieldData.m_FieldInfo);
|
||||
}
|
||||
|
||||
typeData.m_Fields.Add(fieldData.m_FieldInfo.Name, fieldData);
|
||||
}
|
||||
m_TypeDataCache.Add(assemblyQualifiedTypeName, typeData);
|
||||
}
|
||||
return typeData;
|
||||
}
|
||||
|
||||
// Create a function object to set the value of a field of type 'SetterType'. Calling this compiled function object is much faster than using the reflection API
|
||||
static Func<object, SetterType, SetterType> CreateSetter<SetterType>(Type type, FieldInfo field)
|
||||
{
|
||||
ParameterExpression valueExp = Expression.Parameter(field.FieldType, "value");
|
||||
ParameterExpression targetExp = Expression.Parameter(typeof(object), "target");
|
||||
MemberExpression fieldExp = (type.IsValueType && (!type.IsPrimitive)) ? Expression.Field(Expression.Unbox(targetExp, type), field) : Expression.Field(Expression.Convert(targetExp, type), field);
|
||||
return Expression.Lambda<Func<object, SetterType, SetterType>>(Expression.Assign(fieldExp, valueExp), targetExp, valueExp).Compile();
|
||||
}
|
||||
|
||||
// Create a function object to set the value of a field of generic object type that is stored in the stream with type 'StorageType'. Calling this compiled function object is much faster than using the reflection API
|
||||
static Func<object, StorageType, StorageType> CreateObjectSetter<StorageType>(Type type, FieldInfo field)
|
||||
{
|
||||
ParameterExpression valueExp = Expression.Parameter(typeof(StorageType), "value");
|
||||
ParameterExpression targetExp = Expression.Parameter(typeof(object), "target");
|
||||
MemberExpression fieldExp = (type.IsValueType && (!type.IsPrimitive)) ? Expression.Field(Expression.Unbox(targetExp, type), field) : Expression.Field(Expression.Convert(targetExp, type), field);
|
||||
BinaryExpression assignExp = Expression.Assign(fieldExp, Expression.Convert(valueExp, field.FieldType));
|
||||
return Expression.Lambda<Func<object, StorageType, StorageType>>(Expression.Convert(assignExp, typeof(StorageType)), targetExp, valueExp).Compile();
|
||||
}
|
||||
|
||||
// Create a function object to get the value from a field as a generic object. It is much faster to call this compiled function object than to use the reflection API
|
||||
static Func<object, object> CreateObjectGetter(Type type, FieldInfo field)
|
||||
{
|
||||
ParameterExpression valueExp = Expression.Parameter(typeof(object), "value");
|
||||
return Expression.Lambda<Func<object, object>>(Expression.Convert(Expression.Field(Expression.Convert(valueExp, type), field), typeof(object)), valueExp).Compile();
|
||||
}
|
||||
|
||||
// Return the TypeData for a type from the index of it's type name in the type/field stringtable
|
||||
TypeData GetTypeDataFromTypeNameIndex(int typeNameIndex)
|
||||
{
|
||||
// Populate the m_TypeDataByTypeNameIndex LUT with type data based on this index if not already set so we can get the type info from the index faster in the future
|
||||
if (m_TypeDataByTypeNameIndex[typeNameIndex] == null)
|
||||
{
|
||||
m_TypeDataByTypeNameIndex[typeNameIndex] = GetTypeData(m_TypeStringTable[typeNameIndex]);
|
||||
m_TypeByTypeNameIndex[typeNameIndex] = m_TypeDataByTypeNameIndex[typeNameIndex].m_Type;
|
||||
}
|
||||
return m_TypeDataByTypeNameIndex[typeNameIndex];
|
||||
}
|
||||
|
||||
// Read an object from the stream
|
||||
object ReadObject(int depth)
|
||||
{
|
||||
if (!ReadNullFlag())
|
||||
return null;
|
||||
|
||||
// Get the TypeData for the type of this object
|
||||
TypeData typeData = GetTypeDataFromTypeNameIndex(ReadStringIndex());
|
||||
|
||||
// Give custom object factories a chance to create the instance first as this is faster than the generic Activator call.
|
||||
// If no instance is created (either because no factory is registered for this type or the factory didn't produce an instance) then we call Activator as a fallback
|
||||
object objectRead = typeData.m_ObjectFactory?.Invoke();
|
||||
if (objectRead == null)
|
||||
objectRead = Activator.CreateInstance(typeData.m_Type);
|
||||
|
||||
if (EmitVerboseLog)
|
||||
Debug.Log($"{new String(' ', (depth * 2) - ((depth > 0) ? 1 : 0))}ReadObject({typeData.m_Type.Name}) +{m_Reader.BaseStream.Position}");
|
||||
|
||||
Dictionary<string, FieldData> fields = typeData.m_Fields;
|
||||
|
||||
int numFields = m_Reader.ReadUInt16();
|
||||
|
||||
// Initialise the index of field number -> field data for this type if it's the first time we've seen it
|
||||
if (typeData.m_FieldIndex == null)
|
||||
typeData.m_FieldIndex = new FieldData[numFields];
|
||||
|
||||
for (int fieldNum = 0; fieldNum < numFields; fieldNum++)
|
||||
{
|
||||
string fieldName = m_TypeStringTable[ReadStringIndex()];
|
||||
|
||||
FieldData field;
|
||||
if (typeData.m_FieldIndex[fieldNum] != null)
|
||||
field = typeData.m_FieldIndex[fieldNum];
|
||||
else
|
||||
{
|
||||
if (fields.TryGetValue(fieldName, out field))
|
||||
typeData.m_FieldIndex[fieldNum] = field;
|
||||
}
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
DataType fieldDataType = (DataType)m_Reader.ReadByte();
|
||||
|
||||
if (EmitVerboseLog)
|
||||
Debug.Log($"{new String(' ', depth * 2)}Field {fieldName} -> {field?.m_FieldInfo.Name} ({fieldDataType}) +{m_Reader.BaseStream.Position}");
|
||||
|
||||
FieldInfo fieldInfo = field.m_FieldInfo;
|
||||
|
||||
switch (fieldDataType)
|
||||
{
|
||||
case DataType.Byte:
|
||||
((Func<object, byte, byte>)field.m_Setter)(objectRead, m_Reader.ReadByte());
|
||||
break;
|
||||
|
||||
case DataType.Bool:
|
||||
((Func<object, bool, bool>)field.m_Setter)(objectRead, m_Reader.ReadBoolean());
|
||||
break;
|
||||
|
||||
case DataType.Int:
|
||||
((Func<object, int, int>)field.m_Setter)(objectRead, m_Reader.ReadInt32());
|
||||
break;
|
||||
|
||||
case DataType.UInt:
|
||||
((Func<object, uint, uint>)field.m_Setter)(objectRead, m_Reader.ReadUInt32());
|
||||
break;
|
||||
|
||||
case DataType.Long:
|
||||
((Func<object, long, long>)field.m_Setter)(objectRead, m_Reader.ReadInt64());
|
||||
break;
|
||||
|
||||
case DataType.ULong:
|
||||
((Func<object, ulong, ulong>)field.m_Setter)(objectRead, m_Reader.ReadUInt64());
|
||||
break;
|
||||
|
||||
case DataType.Enum:
|
||||
((Func<object, int, int>)field.m_Setter)(objectRead, m_Reader.ReadInt32());
|
||||
break;
|
||||
|
||||
case DataType.String:
|
||||
((Func<object, string, string>)field.m_Setter)(objectRead, ReadString());
|
||||
break;
|
||||
|
||||
case DataType.Type:
|
||||
((Func<object, Type, Type>)field.m_Setter)(objectRead, GetTypeFromCache(ReadStringIndex()));
|
||||
break;
|
||||
|
||||
case DataType.Class:
|
||||
fieldInfo.SetValue(objectRead, ReadObject(depth + 1));
|
||||
break;
|
||||
|
||||
case DataType.Struct:
|
||||
{
|
||||
object structObject = ReadObject(depth + 1);
|
||||
((Func<object, object, object>)field.m_Setter)(objectRead, structObject);
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Array:
|
||||
if (ReadNullFlag())
|
||||
{
|
||||
Array fieldArray;
|
||||
if (field.m_Getter != null)
|
||||
fieldArray = (Array)((Func<object, object>)field.m_Getter)(objectRead);
|
||||
else
|
||||
fieldArray = (Array)fieldInfo.GetValue(objectRead);
|
||||
|
||||
int rank = m_Reader.ReadInt32();
|
||||
if (rank != 1)
|
||||
throw new InvalidDataException($"USerialize currently doesn't support arrays with ranks other than one - data for field {fieldInfo.Name} of type {fieldInfo.FieldType.Name} has rank {rank}");
|
||||
int length = m_Reader.ReadInt32();
|
||||
|
||||
DataType elementDataType = (DataType)m_Reader.ReadByte();
|
||||
|
||||
if (EmitVerboseLog)
|
||||
Debug.Log($"{new String(' ', (depth + 1) * 2)}Array {elementDataType} [{length}] +{m_Reader.BaseStream.Position}");
|
||||
|
||||
switch (elementDataType)
|
||||
{
|
||||
case DataType.Byte:
|
||||
{
|
||||
byte[] byteArray = (byte[])Array.CreateInstance(typeof(byte), length);
|
||||
((Func<object, byte[], byte[]>)field.m_Setter)(objectRead, byteArray);
|
||||
m_Reader.Read(byteArray, 0, length);
|
||||
break;
|
||||
}
|
||||
|
||||
// Per customer request
|
||||
case DataType.ULong:
|
||||
{
|
||||
ulong[] ulongArray = (ulong[])Array.CreateInstance(typeof(ulong), length);
|
||||
((Func<object, ulong[], ulong[]>)field.m_Setter)(objectRead, ulongArray);
|
||||
for (int elementNum = 0; elementNum < length; elementNum++)
|
||||
{
|
||||
ulongArray[elementNum] = m_Reader.ReadUInt64();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.String:
|
||||
{
|
||||
string[] stringArray = (string[])Array.CreateInstance(typeof(string), length);
|
||||
((Func<object, string[], string[]>)field.m_Setter)(objectRead, stringArray);
|
||||
for (int elementNum = 0; elementNum < length; elementNum++)
|
||||
{
|
||||
stringArray[elementNum] = ReadString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Type:
|
||||
{
|
||||
Type[] typeArray = new Type[length];
|
||||
((Func<object, Type[], Type[]>)field.m_Setter)(objectRead, typeArray);
|
||||
for (int elementNum = 0; elementNum < length; elementNum++)
|
||||
{
|
||||
int elementTypeNameIndex = ReadStringIndex();
|
||||
if (elementTypeNameIndex != USerialize.InvalidStringIndex)
|
||||
{
|
||||
typeArray[elementNum] = GetTypeFromCache(elementTypeNameIndex);
|
||||
if (typeArray[elementNum] == null)
|
||||
throw new InvalidDataException($"Could not create Type for '{m_TypeStringTable[elementTypeNameIndex]}'");
|
||||
}
|
||||
|
||||
if (EmitVerboseLog)
|
||||
Debug.Log($"{new String(' ', (depth + 2) * 2)}Type[{elementNum}] = '{m_TypeStringTable[elementTypeNameIndex]}' +{m_Reader.BaseStream.Position}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Class:
|
||||
{
|
||||
Type arrayElementType = GetTypeFromCache(ReadStringIndex());
|
||||
Array classArray = Array.CreateInstance(arrayElementType, length);
|
||||
fieldInfo.SetValue(objectRead, classArray);
|
||||
for (int elementNum = 0; elementNum < length; elementNum++)
|
||||
{
|
||||
DataType objectDataType = (DataType)m_Reader.ReadByte();
|
||||
object elementObject = null;
|
||||
switch (objectDataType)
|
||||
{
|
||||
case DataType.Class:
|
||||
elementObject = ReadObject(depth + 2);
|
||||
break;
|
||||
|
||||
case DataType.String:
|
||||
elementObject = ReadString();
|
||||
break;
|
||||
|
||||
case DataType.Int:
|
||||
elementObject = m_Reader.ReadInt32();
|
||||
break;
|
||||
|
||||
case DataType.Custom:
|
||||
elementObject = ReadCustomObject();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidDataException($"Found unsupported data type '{objectDataType}' in class array '{typeData.m_Type.Name}.{fieldName}'");
|
||||
}
|
||||
classArray.SetValue(elementObject, elementNum);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Struct:
|
||||
{
|
||||
Type elementType = GetTypeFromCache(ReadStringIndex());
|
||||
Array array = Array.CreateInstance(elementType, length);
|
||||
fieldInfo.SetValue(objectRead, array);
|
||||
for (int elementNum = 0; elementNum < length; elementNum++)
|
||||
{
|
||||
array.SetValue(ReadObject(depth + 2), elementNum);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InvalidDataException($"Unknown array element type {elementDataType} for field {fieldInfo.FieldType.Name}.{fieldInfo.Name}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case DataType.List:
|
||||
if (ReadNullFlag())
|
||||
{
|
||||
int count = m_Reader.ReadInt32();
|
||||
Type listType = GetTypeFromCache(ReadStringIndex());
|
||||
|
||||
if (EmitVerboseLog)
|
||||
Debug.Log($"{new String(' ', (depth + 1) * 2)}List {listType.Name} [{count}] +{m_Reader.BaseStream.Position}");
|
||||
|
||||
System.Collections.IList list = (System.Collections.IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(listType), count);
|
||||
for (int itemIndex = 0; itemIndex < count; itemIndex++)
|
||||
{
|
||||
object item = ReadObject(depth + 2);
|
||||
if (item != null)
|
||||
list.Add(item);
|
||||
}
|
||||
fieldInfo.SetValue(objectRead, list);
|
||||
}
|
||||
break;
|
||||
|
||||
case DataType.Custom:
|
||||
fieldInfo.SetValue(objectRead, ReadCustomObject());
|
||||
break;
|
||||
|
||||
case DataType.Guid:
|
||||
{
|
||||
GUID guid;
|
||||
unsafe
|
||||
{
|
||||
UInt64* guidPtr = (UInt64*)&guid;
|
||||
guidPtr[0] = m_Reader.ReadUInt64();
|
||||
guidPtr[1] = m_Reader.ReadUInt64();
|
||||
}
|
||||
((Func<object, GUID, GUID>)field.m_Setter)(objectRead, guid);
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Hash128:
|
||||
{
|
||||
Hash128 hash;
|
||||
unsafe
|
||||
{
|
||||
UInt64* hashPtr = (UInt64*)&hash;
|
||||
hashPtr[0] = m_Reader.ReadUInt64();
|
||||
hashPtr[1] = m_Reader.ReadUInt64();
|
||||
}
|
||||
((Func<object, Hash128, Hash128>)field.m_Setter)(objectRead, hash);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InvalidDataException($"USerialize found unknown field data type '{fieldDataType}' on field '{typeData.m_Type.Name}.{fieldName}' in stream at +{m_Reader.BaseStream.Position}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Didn't find a matching field in the object
|
||||
throw new InvalidDataException($"USerialize found unknown field '{fieldName}' for type '{typeData.m_Type.Name}' in stream at +{m_Reader.BaseStream.Position}");
|
||||
}
|
||||
}
|
||||
|
||||
return objectRead;
|
||||
}
|
||||
|
||||
// Read and deserialize data for an object that was serialized with a custom serializer
|
||||
object ReadCustomObject()
|
||||
{
|
||||
Type objectType = GetTypeFromCache(ReadStringIndex());
|
||||
if (m_CustomSerializers.TryGetValue(objectType, out ICustomSerializer customSerializer))
|
||||
{
|
||||
return customSerializer.UDeSerializer(this);
|
||||
}
|
||||
else
|
||||
throw new InvalidDataException($"Could not find custom deserializer for type {objectType.Name}, custom deserializers can be added prior to deserialization with AddCustomDeserializer()");
|
||||
}
|
||||
|
||||
// Read a byte from the stream and return true if it has value NotNull (1)
|
||||
internal bool ReadNullFlag()
|
||||
{
|
||||
return (m_Reader.ReadByte() == Serializer.NotNull);
|
||||
}
|
||||
|
||||
// Read a byte array from the stream. Will return null if the array was null when serialized
|
||||
internal byte[] ReadBytes()
|
||||
{
|
||||
byte[] bytes = null;
|
||||
if (ReadNullFlag())
|
||||
{
|
||||
bytes = new byte[m_Reader.ReadInt32()];
|
||||
m_Reader.Read(bytes, 0, bytes.Length);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Read a potentially null value string from the stream
|
||||
internal string ReadString()
|
||||
{
|
||||
if (ReadNullFlag())
|
||||
return m_DataStringTable[ReadStringIndex()];
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return a Type instance for a type from the type/field stringtable index of it's name. Uses a cache of previously seen types to improve performance
|
||||
Type GetTypeFromCache(int typeNameIndex)
|
||||
{
|
||||
if (typeNameIndex >= 0)
|
||||
{
|
||||
if (m_TypeByTypeNameIndex[typeNameIndex] == null)
|
||||
m_TypeByTypeNameIndex[typeNameIndex] = Type.GetType(m_TypeStringTable[typeNameIndex]);
|
||||
|
||||
return m_TypeByTypeNameIndex[typeNameIndex];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read a string table index. There are almost never more than 32,767 strings so we use 15 bits by default for compactness.
|
||||
// If a string has an index more than 32,767 (i.e. 0x8000+) we store 0x8000 as a flag to signify this combined with the bottom 15 bits of the index. Bits 15 to 30 are stored in the following 16 bits of data.
|
||||
internal int ReadStringIndex()
|
||||
{
|
||||
int stringIndex = m_Reader.ReadUInt16();
|
||||
if ((stringIndex & 0x8000) != 0)
|
||||
{
|
||||
stringIndex = (stringIndex & 0x7FFF) | (((int)m_Reader.ReadUInt16()) << 15);
|
||||
}
|
||||
return stringIndex;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c8609290d91434541b2f561c09dc7b3b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,683 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities.USerialize
|
||||
{
|
||||
/*
|
||||
* Main USerialize serialzation class. Used to write instances of types to a stream
|
||||
*
|
||||
* To Serialize an object to a stream use code such as
|
||||
*
|
||||
* MemoryStream stream = new MemoryStream();
|
||||
* USerialize.Serializer serializer = new USerialize.Serializer();
|
||||
* serializer.Serialize(stream, myClassInstance, 1); // '1' is the version of our object, we can read this value back at deserialization time if we want to determine the version of our data
|
||||
*/
|
||||
internal class Serializer
|
||||
{
|
||||
// Data for a single field of a single type we have serialized. We cache information obtained from the reflection API here as the reflection API can be slow to query
|
||||
internal class FieldData
|
||||
{
|
||||
internal FieldInfo m_FieldInfo;
|
||||
internal string m_Name;
|
||||
internal int m_NameIndex;
|
||||
internal Type m_ElementType;
|
||||
internal bool m_ElementTypeIsPrimitive;
|
||||
internal bool m_ElementTypeIsClass;
|
||||
internal bool m_ElementTypeIsValueType;
|
||||
internal DataType m_DataType = DataType.Invalid;
|
||||
internal object m_Getter;
|
||||
}
|
||||
|
||||
// Data for a type we have serialized. We cache information obtained from the reflection API here as the reflection API can be slow to query
|
||||
internal class TypeData
|
||||
{
|
||||
internal FieldData[] m_Fields;
|
||||
internal string m_AssemblyQualifiedName;
|
||||
internal int m_AssemblyQualifiedNameIndex;
|
||||
}
|
||||
|
||||
// Simple string table, our serialized data contains two of these, one for the names of the types and fields serialized and one for the values of any string fields or arrays/lists using strings
|
||||
internal class StringTable
|
||||
{
|
||||
List<string> m_Strings = new List<string>();
|
||||
Dictionary<string, int> m_Index = new Dictionary<string, int>();
|
||||
|
||||
/// <summary>
|
||||
/// Clear the data in this stringtable to make it empty
|
||||
/// </summary>
|
||||
internal void Clear()
|
||||
{
|
||||
m_Index.Clear();
|
||||
m_Strings.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the strings from this stringtable to a binary writer one after another
|
||||
/// </summary>
|
||||
/// <param name="writer">Writer to write the strings to</param>
|
||||
/// <returns>the byte position in the stream being written to where the strings start, this is the current position in the stream when the function was called</returns>
|
||||
internal long Write(BinaryWriter writer)
|
||||
{
|
||||
long stringTableBytePosition = writer.Seek(0, SeekOrigin.Current);
|
||||
|
||||
writer.Write(m_Strings.Count);
|
||||
m_Strings.ForEach((item) => writer.Write(item));
|
||||
|
||||
return stringTableBytePosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the index of a string in the stringtable if it exists, if it does not exist add it then return it's index
|
||||
/// </summary>
|
||||
/// <param name="stringToAddOrFind"></param>
|
||||
/// <returns></returns>
|
||||
internal int GetStringIndex(string stringToAddOrFind)
|
||||
{
|
||||
if (!m_Index.TryGetValue(stringToAddOrFind, out int stringIndex))
|
||||
{
|
||||
stringIndex = m_Strings.Count;
|
||||
m_Strings.Add(stringToAddOrFind);
|
||||
m_Index.Add(stringToAddOrFind, stringIndex);
|
||||
}
|
||||
return stringIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Byte values we store in the stream to signify whether a reference type is null (and thus absent from the stream) or not (and thus it's data comes next)
|
||||
internal const byte IsNull = 0;
|
||||
internal const byte NotNull = 1;
|
||||
|
||||
// Custom serializers can be provided by the client code to implement serialization for types that cannot be adequately handled by the generic reflection based code
|
||||
// Client code can pass an array of custom serializers and their associated types to the Serializer constructor or can call AddCustomSerializer() to add individual custom serializers at any time prior to serialization taking place
|
||||
Dictionary<Type, ICustomSerializer> m_CustomSerializers = new Dictionary<Type, ICustomSerializer>();
|
||||
|
||||
// Cache of TypeData instances for all the types we've been asked to serialize so far. Only type specific data is stored here not instance specific data and this cache *is not* cleared between calls to Serialize() so using the same
|
||||
// Serializer instance to write multiple instances of the same types achieves a significant performance benefit by being able to re-use type information without having to call slow reflection APIs again.
|
||||
Dictionary<Type, TypeData> m_TypeDataCache = new Dictionary<Type, TypeData>();
|
||||
|
||||
// Accessing Type.AssemblyQualifiedName can be slow so we keep this cache mapping types to the string table indices in the type/field string table of the assembly qualified name of types being serialized.
|
||||
// Each time Serialize() is called new stringtables are emitted so this is cleared before each call but still provides a measurable speed increase vs. accessing Type.AssemblyQualifiedName each time it's needed
|
||||
Dictionary<Type, int> m_TypeQualifiedNameIndices = new Dictionary<Type, int>();
|
||||
|
||||
// String table of strings from field values encountered
|
||||
StringTable m_DataStringTable = new StringTable();
|
||||
|
||||
// String table of type and field names encountered
|
||||
StringTable m_TypeStringTable = new StringTable();
|
||||
|
||||
// Writer we are writing out serialized data to
|
||||
BinaryWriter m_Writer;
|
||||
|
||||
// Serialization data format version number. Written to the stream to provide a means for upgrade should it be necessary in the future
|
||||
internal const byte SerializationVersion = 1;
|
||||
|
||||
internal Serializer()
|
||||
{
|
||||
}
|
||||
|
||||
internal Serializer(params ICustomSerializer[] customSerializers)
|
||||
{
|
||||
if (customSerializers != null)
|
||||
Array.ForEach(customSerializers, (customSerializer) => AddCustomSerializer(customSerializer));
|
||||
}
|
||||
|
||||
internal void AddCustomSerializer(ICustomSerializer customSerializer)
|
||||
{
|
||||
m_CustomSerializers.Add(customSerializer.GetType(), customSerializer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear data that we cache about types and object contents that can change between objects.
|
||||
/// </summary>
|
||||
void ClearPerObjectCachedData()
|
||||
{
|
||||
// Reset the type/field name and data string tables to empty for each object.
|
||||
m_DataStringTable.Clear();
|
||||
m_TypeStringTable.Clear();
|
||||
|
||||
// Clear the type and field name indices from the cached type data as each object's type string table is distinct so the indices from any previously written objects will be invalid
|
||||
foreach (KeyValuePair<Type, TypeData> typeDataCacheEntry in m_TypeDataCache)
|
||||
{
|
||||
typeDataCacheEntry.Value.m_AssemblyQualifiedNameIndex = -1;
|
||||
|
||||
foreach (FieldData fieldData in typeDataCacheEntry.Value.m_Fields)
|
||||
{
|
||||
fieldData.m_NameIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the assembly qualified type name cache as the indices of the assembly qualified type names in the type stringtable will likely be different for this object than the previous one serialized
|
||||
m_TypeQualifiedNameIndices.Clear();
|
||||
}
|
||||
|
||||
|
||||
// Main serialization function. Serializes 'objectToSerialize' to the given stream inserting the supplied object version number in the data. The object version number can be obtained by DeSerializer.ObjectVersion when deserializing the data
|
||||
internal void Serialize(Stream stream, object objectToSerialize, int objectVersion)
|
||||
{
|
||||
ClearPerObjectCachedData();
|
||||
|
||||
m_Writer = new BinaryWriter(stream);
|
||||
|
||||
// Version number for the serialization file format itself
|
||||
m_Writer.Write(SerializationVersion);
|
||||
|
||||
// Client code version number for their own use
|
||||
m_Writer.Write(objectVersion);
|
||||
|
||||
// Leave space for the offsets to the type and data stringtables data that we write after the serialization data proper
|
||||
long stringTableOffsetPosition = m_Writer.Seek(0, SeekOrigin.Current);
|
||||
m_Writer.Write(0uL); // Space for the offset to the type string table data
|
||||
m_Writer.Write(0uL); // Space for the offset to the data string table data
|
||||
|
||||
// Write serialization data for the object
|
||||
WriteObject(objectToSerialize);
|
||||
|
||||
// Write the type and data stringtables then fill in their position offsets at the start of the data we left space for earlier
|
||||
long typeStringTableBytePos = m_TypeStringTable.Write(m_Writer);
|
||||
long dataStringTableBytePos = m_DataStringTable.Write(m_Writer);
|
||||
|
||||
m_Writer.Seek((int)stringTableOffsetPosition, SeekOrigin.Begin);
|
||||
m_Writer.Write(typeStringTableBytePos);
|
||||
m_Writer.Write(dataStringTableBytePos);
|
||||
|
||||
m_Writer.Flush();
|
||||
}
|
||||
|
||||
// Call to start writing directly to a stream, used primarily for testing USerialize functions in isolation
|
||||
internal void StartWritingToStream(Stream stream)
|
||||
{
|
||||
m_Writer = new BinaryWriter(stream);
|
||||
}
|
||||
|
||||
// Call when we've finished writing to a stream, used primarily for testing USerialize functions in isolation
|
||||
internal void FinishWritingToStream()
|
||||
{
|
||||
m_Writer.Flush();
|
||||
}
|
||||
|
||||
// Return the cached type data for a given type. Will return it from the m_TypeDataCache cache if present otherwise will generate a new TypeData instance from the type and add it to the cache
|
||||
TypeData GetTypeData(Type type)
|
||||
{
|
||||
if (!m_TypeDataCache.TryGetValue(type, out TypeData typeData))
|
||||
{
|
||||
// Cache data about the fields that is slow to retrieve every time an instance of this type is processed
|
||||
|
||||
FieldInfo[] fieldInfos = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
typeData = new TypeData() { m_Fields = new FieldData[fieldInfos.Length] };
|
||||
typeData.m_AssemblyQualifiedName = type.AssemblyQualifiedName;
|
||||
typeData.m_AssemblyQualifiedNameIndex = m_TypeStringTable.GetStringIndex(typeData.m_AssemblyQualifiedName);
|
||||
|
||||
for (int fieldNum = 0; fieldNum < fieldInfos.Length; fieldNum++)
|
||||
{
|
||||
FieldInfo field = fieldInfos[fieldNum];
|
||||
FieldData fieldData = new FieldData();
|
||||
fieldData.m_FieldInfo = field;
|
||||
fieldData.m_Name = field.Name;
|
||||
fieldData.m_NameIndex = m_TypeStringTable.GetStringIndex(fieldData.m_Name);
|
||||
|
||||
if (typeof(Array).IsAssignableFrom(field.FieldType))
|
||||
{
|
||||
fieldData.m_DataType = DataType.Array;
|
||||
fieldData.m_ElementType = field.FieldType.GetElementType();
|
||||
fieldData.m_ElementTypeIsPrimitive = fieldData.m_ElementType.IsPrimitive;
|
||||
fieldData.m_ElementTypeIsClass = fieldData.m_ElementType.IsClass;
|
||||
fieldData.m_ElementTypeIsValueType = fieldData.m_ElementType.IsValueType;
|
||||
fieldData.m_Getter = CreateObjectGetter(type, field);
|
||||
}
|
||||
else if (field.FieldType.IsGenericType && (field.FieldType.GetGenericTypeDefinition() == typeof(List<>)))
|
||||
{
|
||||
fieldData.m_DataType = DataType.List;
|
||||
fieldData.m_ElementType = field.FieldType.GetGenericArguments()[0];
|
||||
}
|
||||
else if (field.FieldType == typeof(GUID))
|
||||
{
|
||||
fieldData.m_DataType = DataType.Guid;
|
||||
fieldData.m_Getter = CreateGetter<GUID>(type, field);
|
||||
}
|
||||
else if (field.FieldType == typeof(Hash128))
|
||||
{
|
||||
fieldData.m_DataType = DataType.Hash128;
|
||||
fieldData.m_Getter = CreateGetter<Hash128>(type, field);
|
||||
}
|
||||
else if (field.FieldType.IsEnum)
|
||||
{
|
||||
fieldData.m_DataType = DataType.Enum;
|
||||
fieldData.m_Getter = CreateObjectGetter(type, field);
|
||||
}
|
||||
else if (field.FieldType == typeof(String))
|
||||
{
|
||||
fieldData.m_DataType = DataType.String;
|
||||
fieldData.m_Getter = CreateGetter<string>(type, field);
|
||||
}
|
||||
else if (field.FieldType.IsClass)
|
||||
{
|
||||
fieldData.m_DataType = DataType.Class;
|
||||
fieldData.m_Getter = CreateObjectGetter(type, field);
|
||||
}
|
||||
else if (field.FieldType.IsValueType && (!field.FieldType.IsPrimitive))
|
||||
{
|
||||
fieldData.m_DataType = DataType.Struct;
|
||||
fieldData.m_Getter = CreateObjectGetter(type, field);
|
||||
}
|
||||
else if (field.FieldType == typeof(byte))
|
||||
{
|
||||
fieldData.m_DataType = DataType.Byte;
|
||||
fieldData.m_Getter = CreateGetter<byte>(type, field);
|
||||
}
|
||||
else if (field.FieldType == typeof(bool))
|
||||
{
|
||||
fieldData.m_DataType = DataType.Bool;
|
||||
fieldData.m_Getter = CreateGetter<bool>(type, field);
|
||||
}
|
||||
else if (field.FieldType == typeof(int))
|
||||
{
|
||||
fieldData.m_DataType = DataType.Int;
|
||||
fieldData.m_Getter = CreateGetter<int>(type, field);
|
||||
}
|
||||
else if (field.FieldType == typeof(uint))
|
||||
{
|
||||
fieldData.m_DataType = DataType.UInt;
|
||||
fieldData.m_Getter = CreateGetter<uint>(type, field);
|
||||
}
|
||||
else if (field.FieldType == typeof(long))
|
||||
{
|
||||
fieldData.m_DataType = DataType.Long;
|
||||
fieldData.m_Getter = CreateGetter<long>(type, field);
|
||||
}
|
||||
else if (field.FieldType == typeof(ulong))
|
||||
{
|
||||
fieldData.m_DataType = DataType.ULong;
|
||||
fieldData.m_Getter = CreateGetter<ulong>(type, field);
|
||||
}
|
||||
else if (field.FieldType == typeof(Type))
|
||||
{
|
||||
fieldData.m_DataType = DataType.Type;
|
||||
fieldData.m_Getter = CreateGetter<Type>(type, field);
|
||||
}
|
||||
|
||||
typeData.m_Fields[fieldNum] = fieldData;
|
||||
}
|
||||
m_TypeDataCache.Add(type, typeData);
|
||||
}
|
||||
else if (typeData.m_AssemblyQualifiedNameIndex == -1)
|
||||
{
|
||||
// This type is in our cache but it hasn't been used by the object being serialized yet. Find/add it's type and field names to the type string table so we have a valid index
|
||||
typeData.m_AssemblyQualifiedNameIndex = m_TypeStringTable.GetStringIndex(typeData.m_AssemblyQualifiedName);
|
||||
foreach (FieldData fieldData in typeData.m_Fields)
|
||||
{
|
||||
fieldData.m_NameIndex = m_TypeStringTable.GetStringIndex(fieldData.m_Name);
|
||||
}
|
||||
|
||||
}
|
||||
return typeData;
|
||||
}
|
||||
|
||||
// Create a function object to get the value from a field of type 'GetterType'. It is much faster to call this compiled function object than to use the reflection API
|
||||
static Func<object, GetterType> CreateGetter<GetterType>(Type type, FieldInfo field)
|
||||
{
|
||||
ParameterExpression valueExp = Expression.Parameter(typeof(object), "value");
|
||||
return Expression.Lambda<Func<object, GetterType>>(Expression.Field(Expression.Convert(valueExp, type), field), valueExp).Compile();
|
||||
}
|
||||
|
||||
// Create a function object to get the value from a field as a generic object. It is much faster to call this compiled function object than to use the reflection API
|
||||
static Func<object, object> CreateObjectGetter(Type type, FieldInfo field)
|
||||
{
|
||||
ParameterExpression valueExp = Expression.Parameter(typeof(object), "value");
|
||||
return Expression.Lambda<Func<object, object>>(Expression.Convert(Expression.Field(Expression.Convert(valueExp, type), field), typeof(object)), valueExp).Compile();
|
||||
}
|
||||
|
||||
// Write an object to the serialization stream
|
||||
void WriteObject(object objectToWrite)
|
||||
{
|
||||
if (!WriteNullFlag(objectToWrite))
|
||||
return;
|
||||
|
||||
// Get information about the objects type then write the type/field stringtable index of it's type name and how many fields it has
|
||||
Type objectType = objectToWrite.GetType();
|
||||
TypeData typeData = GetTypeData(objectType);
|
||||
|
||||
WriteStringIndex(typeData.m_AssemblyQualifiedNameIndex);
|
||||
|
||||
if (typeData.m_Fields.Length > ushort.MaxValue)
|
||||
throw new InvalidDataException($"USerialize cannot serialize objects with more than {ushort.MaxValue} fields");
|
||||
m_Writer.Write((ushort)typeData.m_Fields.Length);
|
||||
|
||||
// Process each field in turn
|
||||
foreach (FieldData field in typeData.m_Fields)
|
||||
{
|
||||
switch (field.m_DataType)
|
||||
{
|
||||
case DataType.Array:
|
||||
{
|
||||
WriteFieldInfo(field, DataType.Array);
|
||||
Array array = (Array)((Func<object, object>)field.m_Getter)(objectToWrite);
|
||||
if (WriteNullFlag(array))
|
||||
{
|
||||
// We only support rank 1 for now
|
||||
m_Writer.Write(array.Rank);
|
||||
m_Writer.Write(array.Length);
|
||||
|
||||
if (array.Rank != 1)
|
||||
throw new InvalidDataException($"USerialize currently doesn't support arrays with ranks other than one - field {field.m_Name} of type {field.m_FieldInfo.FieldType.Name} has rank {array.Rank}");
|
||||
|
||||
Type elementType = field.m_ElementType;
|
||||
if (field.m_ElementTypeIsPrimitive)
|
||||
{
|
||||
// A primitive array, write the bytes as optimally as possible for types we support
|
||||
if (elementType == typeof(byte))
|
||||
{
|
||||
// byte[]
|
||||
m_Writer.Write((byte)DataType.Byte);
|
||||
m_Writer.Write((byte[])array, 0, array.Length);
|
||||
}
|
||||
// Per customer request
|
||||
else if (elementType == typeof(ulong))
|
||||
{
|
||||
ulong[] ulongArray = (ulong[])array;
|
||||
m_Writer.Write((byte)DataType.ULong);
|
||||
for (int elementIndex = 0; elementIndex < array.Length; elementIndex++)
|
||||
{
|
||||
m_Writer.Write(ulongArray[elementIndex]);
|
||||
}
|
||||
}
|
||||
else
|
||||
throw new InvalidDataException($"USerialize currently doesn't support primitive arrays of type {elementType.Name} - field {field.m_Name} of type {field.m_FieldInfo.FieldType.Name}");
|
||||
}
|
||||
else if (elementType == typeof(string))
|
||||
{
|
||||
// String[]
|
||||
string[] stringArray = (string[])array;
|
||||
m_Writer.Write((byte)DataType.String);
|
||||
for (int elementIndex = 0; elementIndex < array.Length; elementIndex++)
|
||||
{
|
||||
WriteDataString(stringArray[elementIndex]);
|
||||
}
|
||||
}
|
||||
else if (elementType == typeof(Type))
|
||||
{
|
||||
// Type[]
|
||||
Type[] typeArray = (Type[])array;
|
||||
m_Writer.Write((byte)DataType.Type);
|
||||
for (int elementIndex = 0; elementIndex < array.Length; elementIndex++)
|
||||
{
|
||||
if (typeArray[elementIndex] != null)
|
||||
WriteStringIndex(GetTypeQualifiedNameIndex(typeArray[elementIndex]));
|
||||
else
|
||||
WriteStringIndex(USerialize.InvalidStringIndex);
|
||||
|
||||
}
|
||||
}
|
||||
else if (field.m_ElementTypeIsClass)
|
||||
{
|
||||
// An array of class instances
|
||||
m_Writer.Write((byte)DataType.Class);
|
||||
WriteStringIndex(GetTypeQualifiedNameIndex(elementType));
|
||||
for (int elementIndex = 0; elementIndex < array.Length; elementIndex++)
|
||||
{
|
||||
object elementToWrite = array.GetValue(elementIndex);
|
||||
|
||||
// If the element isn't null see if we have a custom serializer for the type of this instance.
|
||||
// The array type might be a base class for the actual instances which may not all be the same derived type so we use the runtime type of each instance individually to check for custom serializers rather than using the type of the array itself
|
||||
if (elementToWrite != null)
|
||||
{
|
||||
Type elementObjectType = elementToWrite.GetType();
|
||||
if (m_CustomSerializers.TryGetValue(elementObjectType, out ICustomSerializer customSerializer))
|
||||
{
|
||||
m_Writer.Write((byte)DataType.Custom);
|
||||
WriteStringIndex(GetTypeQualifiedNameIndex(elementObjectType));
|
||||
customSerializer.USerializer(this, elementToWrite);
|
||||
}
|
||||
else if (elementObjectType == typeof(string))
|
||||
{
|
||||
m_Writer.Write((byte)DataType.String);
|
||||
WriteDataString((string)elementToWrite);
|
||||
}
|
||||
else if (elementObjectType == typeof(Int32))
|
||||
{
|
||||
m_Writer.Write((byte)DataType.Int);
|
||||
m_Writer.Write((int)elementToWrite);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (elementObjectType.IsPrimitive)
|
||||
throw new InvalidDataException($"USerialize cannot handle type '{elementObjectType.Name}' in object[] array '{objectType.Name}.{field.m_Name}'");
|
||||
m_Writer.Write((byte)DataType.Class);
|
||||
WriteObject(elementToWrite);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Writer.Write((byte)DataType.Class);
|
||||
m_Writer.Write(IsNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (field.m_ElementTypeIsValueType)
|
||||
{
|
||||
// An array of struct instances
|
||||
m_Writer.Write((byte)DataType.Struct);
|
||||
WriteStringIndex(GetTypeQualifiedNameIndex(elementType));
|
||||
for (int elementIndex = 0; elementIndex < array.Length; elementIndex++)
|
||||
{
|
||||
WriteObject(array.GetValue(elementIndex));
|
||||
}
|
||||
}
|
||||
else
|
||||
throw new InvalidDataException($"USerialize doesn't support serializing array field {field.m_Name} of type {field.m_FieldInfo.FieldType.Name} which is of type {elementType.Name}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.List:
|
||||
{
|
||||
// A List<>
|
||||
WriteFieldInfo(field, DataType.List);
|
||||
System.Collections.IList list = field.m_FieldInfo.GetValue(objectToWrite) as System.Collections.IList;
|
||||
if (WriteNullFlag(list))
|
||||
{
|
||||
m_Writer.Write(list.Count);
|
||||
WriteStringIndex(GetTypeQualifiedNameIndex(field.m_ElementType));
|
||||
for (int elementIndex = 0; elementIndex < list.Count; elementIndex++)
|
||||
{
|
||||
WriteObject(list[elementIndex]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Guid:
|
||||
{
|
||||
// GUID instance
|
||||
WriteFieldInfo(field, DataType.Guid);
|
||||
GUID guid = ((Func<object, GUID>)field.m_Getter)(objectToWrite);
|
||||
unsafe
|
||||
{
|
||||
UInt64* guidPtr = (UInt64*)&guid;
|
||||
m_Writer.Write(guidPtr[0]);
|
||||
m_Writer.Write(guidPtr[1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Hash128:
|
||||
{
|
||||
// Hash128 instance
|
||||
WriteFieldInfo(field, DataType.Hash128);
|
||||
Hash128 hash = ((Func<object, Hash128>)field.m_Getter)(objectToWrite);
|
||||
unsafe
|
||||
{
|
||||
UInt64* hashPtr = (UInt64*)&hash;
|
||||
m_Writer.Write(hashPtr[0]);
|
||||
m_Writer.Write(hashPtr[1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Enum:
|
||||
// An enum, we write it's value as an Int32
|
||||
WriteFieldInfo(field, DataType.Enum);
|
||||
m_Writer.Write((int)((Func<object, object>)field.m_Getter)(objectToWrite));
|
||||
break;
|
||||
|
||||
case DataType.String:
|
||||
{
|
||||
// String instance
|
||||
WriteFieldInfo(field, DataType.String);
|
||||
WriteDataString(((Func<object, string>)field.m_Getter)(objectToWrite));
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Class:
|
||||
{
|
||||
// Is a class instance. If the value isn't null check to see if we have been given a custom serializer for it's type.
|
||||
// If the value is null or there is no custom serializer registered for the value's type write it as normal
|
||||
// Note the type of the actual object is used to locate custom serializers rather than the type of the field in case the object is actually of a derived type
|
||||
object fieldValue = ((Func<object, object>)field.m_Getter)(objectToWrite);
|
||||
if ((fieldValue == null) || (!DoCustomSerialization(field, fieldValue)))
|
||||
{
|
||||
WriteFieldInfo(field, DataType.Class);
|
||||
WriteObject(fieldValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Struct:
|
||||
{
|
||||
// Is a struct instance. Check to see if we have been given a custom serializer for it's type, if not write it as normal
|
||||
object fieldValue = ((Func<object, object>)field.m_Getter)(objectToWrite);
|
||||
if (!DoCustomSerialization(field, fieldValue))
|
||||
{
|
||||
WriteFieldInfo(field, DataType.Struct);
|
||||
WriteObject(fieldValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Byte:
|
||||
WriteFieldInfo(field, DataType.Byte);
|
||||
m_Writer.Write(((Func<object, byte>)field.m_Getter)(objectToWrite));
|
||||
break;
|
||||
|
||||
case DataType.Bool:
|
||||
WriteFieldInfo(field, DataType.Bool);
|
||||
m_Writer.Write(((Func<object, bool>)field.m_Getter)(objectToWrite));
|
||||
break;
|
||||
|
||||
case DataType.Int:
|
||||
WriteFieldInfo(field, DataType.Int);
|
||||
m_Writer.Write(((Func<object, int>)field.m_Getter)(objectToWrite));
|
||||
break;
|
||||
|
||||
case DataType.UInt:
|
||||
WriteFieldInfo(field, DataType.UInt);
|
||||
m_Writer.Write(((Func<object, uint>)field.m_Getter)(objectToWrite));
|
||||
break;
|
||||
|
||||
case DataType.Long:
|
||||
WriteFieldInfo(field, DataType.Long);
|
||||
m_Writer.Write(((Func<object, long>)field.m_Getter)(objectToWrite));
|
||||
break;
|
||||
|
||||
case DataType.ULong:
|
||||
WriteFieldInfo(field, DataType.ULong);
|
||||
m_Writer.Write(((Func<object, ulong>)field.m_Getter)(objectToWrite));
|
||||
break;
|
||||
|
||||
case DataType.Type:
|
||||
WriteFieldInfo(field, DataType.Type);
|
||||
WriteStringIndex(GetTypeQualifiedNameIndex(((Func<object, Type>)field.m_Getter)(objectToWrite)));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidDataException($"USerialize doesn't know how to serialize field {objectType.Name}.{field.m_Name} of type {field.m_FieldInfo.FieldType.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the index in the type/field stringtable of the AssemblyQualifiedName of the given type. Accessing Type.AssemblyQualifiedName can be slow so we use a cache
|
||||
// to store the string table indices of types we've encountered before
|
||||
int GetTypeQualifiedNameIndex(Type type)
|
||||
{
|
||||
if (type == null)
|
||||
return -1;
|
||||
|
||||
if (!m_TypeQualifiedNameIndices.TryGetValue(type, out int qualifiedNameIndex))
|
||||
{
|
||||
qualifiedNameIndex = m_TypeStringTable.GetStringIndex(type.AssemblyQualifiedName);
|
||||
m_TypeQualifiedNameIndices.Add(type, qualifiedNameIndex);
|
||||
}
|
||||
return qualifiedNameIndex;
|
||||
}
|
||||
|
||||
// Check to see if a custom serializer has been registered for the type of a given object. If so call it and return true, otherwise return false
|
||||
bool DoCustomSerialization(FieldData field, object valueToSerialize)
|
||||
{
|
||||
Type valueType = valueToSerialize.GetType();
|
||||
if (m_CustomSerializers.TryGetValue(valueType, out ICustomSerializer customSerializer))
|
||||
{
|
||||
WriteFieldInfo(field, DataType.Custom);
|
||||
WriteStringIndex(GetTypeQualifiedNameIndex(valueType));
|
||||
customSerializer.USerializer(this, valueToSerialize);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write a byte array to the stream. The actual bytes are preceded by a flag byte indicating whether the array passed in was null or not
|
||||
internal void WriteBytes(byte[] bytes)
|
||||
{
|
||||
if (WriteNullFlag(bytes))
|
||||
{
|
||||
m_Writer.Write(bytes.Length);
|
||||
m_Writer.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// If the supplied object *is not* null write a byte with value NotNull (1) to the stream and return true
|
||||
// if the supplied object *is* null write a byte with value IsNull (0) to the stream and return false
|
||||
internal bool WriteNullFlag(object value)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
m_Writer.Write(NotNull);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_Writer.Write(IsNull);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write a string table index. There are almost never more than 32,767 strings so we use 15 bits by default for compactness.
|
||||
// If a string has an index more than 32,767 (i.e. 0x8000+) we store 0x8000 as a flag to signify this combined with the bottom 15 bits of the index. Bits 15 to 30 are stored in the following 16 bits of data.
|
||||
internal void WriteStringIndex(int stringIndex)
|
||||
{
|
||||
if (stringIndex < 0x8000)
|
||||
m_Writer.Write((ushort)stringIndex);
|
||||
else
|
||||
{
|
||||
m_Writer.Write((ushort)(0x8000 | (stringIndex & 0x7FFF)));
|
||||
m_Writer.Write((ushort)(stringIndex >> 15));
|
||||
}
|
||||
}
|
||||
|
||||
// Write meta-data for a field to the stream. The index of the field's name in the type/field stringtable is written followed by a byte indicating the type of the data in the stream
|
||||
void WriteFieldInfo(FieldData field, DataType dataType)
|
||||
{
|
||||
WriteStringIndex(field.m_NameIndex);
|
||||
m_Writer.Write((byte)dataType);
|
||||
}
|
||||
|
||||
// Write a field value string to the stream. A IsNull/NotNull byte is written then if the string is not null the index of the string in the data stringtable
|
||||
internal void WriteDataString(string stringToWrite)
|
||||
{
|
||||
if (WriteNullFlag(stringToWrite))
|
||||
WriteStringIndex(m_DataStringTable.GetStringIndex(stringToWrite));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 947200859c948604e8bbf513d0b1eaf1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,51 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities.USerialize
|
||||
{
|
||||
// Data types that USerialize can emit to the stream
|
||||
internal enum DataType
|
||||
{
|
||||
Byte,
|
||||
Bool,
|
||||
Int,
|
||||
UInt,
|
||||
Long,
|
||||
ULong,
|
||||
String,
|
||||
Class,
|
||||
Struct,
|
||||
Enum,
|
||||
Array,
|
||||
List,
|
||||
Custom,
|
||||
Type,
|
||||
Guid,
|
||||
Hash128,
|
||||
Invalid
|
||||
}
|
||||
|
||||
// Custom serializers can be provided by the client code to implement serialization for types that cannot be adequately handled by the generic reflection based code
|
||||
// A custom serializer is a class that implements this ICustomSerializer interface to provide functions to serialize data for a type to the stream and recreate an instance of the type from a serialized data stream
|
||||
// Client code can pass an array of custom serializers and their associated types to the Serializer/Deserializer constructor or can call AddCustomSerializer() to add individual custom serializers at any time prior to serialization taking place
|
||||
internal interface ICustomSerializer
|
||||
{
|
||||
// Return the type that this custom serializer deals with
|
||||
Type GetType();
|
||||
|
||||
// Serializer function to convert an instance of the type into a serialized stream.
|
||||
void USerializer(Serializer serializer, object value);
|
||||
|
||||
// Deserializer function to create an instance of the type from a previously serialized stream
|
||||
object UDeSerializer(DeSerializer deserializer);
|
||||
}
|
||||
|
||||
internal static class USerialize
|
||||
{
|
||||
// Reserved value for string indices representing an invalid string index, maximum value that can be written by Serializer.WriteStringIndex() or read by DeSerializer.ReadStringIndex()
|
||||
internal const int InvalidStringIndex = int.MaxValue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4e3c19776468fc14296d45fbc07b24a6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 736c37b44570af14e8f8e8e26f0ed941
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,306 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEditor.Build.Content;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities.USerialize
|
||||
{
|
||||
// Uses reflection to recursively produce a text based dump of the types, names and values of all fields in an object and it's sub-objects.
|
||||
// Useful for visual verification or comparison of the data
|
||||
internal class DumpToText
|
||||
{
|
||||
// Custom code to dump a type to text that cannot be properly handled by the default reflection based code
|
||||
internal interface ICustomDumper
|
||||
{
|
||||
// Return the type that this custom dumper deals with
|
||||
Type GetType();
|
||||
|
||||
// Dump an object to text
|
||||
void CustomDumper(DumpToText dumpToText, object value);
|
||||
}
|
||||
|
||||
|
||||
Dictionary<Type, ICustomDumper> m_CustomDumpers = new Dictionary<Type, ICustomDumper>();
|
||||
|
||||
StringBuilder m_StringBuilder = new StringBuilder();
|
||||
string m_Indent = "";
|
||||
|
||||
internal DumpToText()
|
||||
{
|
||||
}
|
||||
|
||||
internal DumpToText(params ICustomDumper[] customDumpers)
|
||||
{
|
||||
if (customDumpers != null)
|
||||
Array.ForEach(customDumpers, (customDumper) => AddCustomDumper(customDumper));
|
||||
}
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
m_StringBuilder = new StringBuilder();
|
||||
m_Indent = "";
|
||||
}
|
||||
|
||||
internal void AddCustomDumper(ICustomDumper customDumper)
|
||||
{
|
||||
m_CustomDumpers.Add(customDumper.GetType(), customDumper);
|
||||
}
|
||||
|
||||
internal string SanitiseFieldName(string fieldName)
|
||||
{
|
||||
if ((fieldName[0] == '<') && fieldName.EndsWith(">k__BackingField"))
|
||||
fieldName = fieldName.Substring(1, fieldName.Length - 17);
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
internal static string SanitiseGenericName(string genericName)
|
||||
{
|
||||
int argSepPos = genericName.IndexOf('`');
|
||||
return (argSepPos != -1) ? genericName.Substring(0, argSepPos) : genericName;
|
||||
}
|
||||
|
||||
internal void Add(string label)
|
||||
{
|
||||
label = SanitiseFieldName(label);
|
||||
|
||||
m_StringBuilder.AppendLine($"{m_Indent}{label}");
|
||||
}
|
||||
|
||||
internal void Add(string label, string value)
|
||||
{
|
||||
label = SanitiseFieldName(label);
|
||||
|
||||
m_StringBuilder.AppendLine($"{m_Indent}{label}: {value}");
|
||||
}
|
||||
|
||||
internal void Add(string typename, string label, string value)
|
||||
{
|
||||
label = SanitiseFieldName(label);
|
||||
|
||||
m_StringBuilder.AppendLine($"{m_Indent}{typename} {label} = {value}");
|
||||
}
|
||||
|
||||
internal void Indent()
|
||||
{
|
||||
m_Indent = m_Indent + " | ";
|
||||
}
|
||||
|
||||
internal void Undent()
|
||||
{
|
||||
m_Indent = m_Indent.Substring(0, m_Indent.Length - 4);
|
||||
}
|
||||
|
||||
// Generate a one line string describing a type. For normal types it's just the type name, for generic type it has the generic arguments included
|
||||
internal static string DescribeType(Type typeToDescribe)
|
||||
{
|
||||
if (!typeToDescribe.IsGenericType)
|
||||
return typeToDescribe.Name;
|
||||
|
||||
Type[] genericArgs = typeToDescribe.GetGenericArguments();
|
||||
|
||||
string argString = $"{SanitiseGenericName(typeToDescribe.Name)}<";
|
||||
if (genericArgs.Length > 0)
|
||||
{
|
||||
if (genericArgs[0].IsGenericType)
|
||||
argString += DescribeType(genericArgs[0]);
|
||||
else
|
||||
argString += genericArgs[0].Name;
|
||||
for (int argNum = 1; argNum < genericArgs.Length; argNum++)
|
||||
{
|
||||
if (genericArgs[argNum].IsGenericType)
|
||||
argString += ", " + DescribeType(genericArgs[argNum]);
|
||||
else
|
||||
argString += ", " + genericArgs[argNum].Name;
|
||||
}
|
||||
}
|
||||
return argString + ">";
|
||||
}
|
||||
|
||||
internal StringBuilder Dump(string label, object thingToDump)
|
||||
{
|
||||
if (thingToDump == null)
|
||||
{
|
||||
Add(label, "<null>");
|
||||
return m_StringBuilder;
|
||||
}
|
||||
|
||||
Type thingType = thingToDump.GetType();
|
||||
if ((thingToDump != null) && (m_CustomDumpers.TryGetValue(thingType, out ICustomDumper customDumper)))
|
||||
{
|
||||
Add(label);
|
||||
customDumper.CustomDumper(this, thingToDump);
|
||||
return m_StringBuilder;
|
||||
}
|
||||
|
||||
Add(label);
|
||||
|
||||
if (m_Indent.Length > 256)
|
||||
{
|
||||
m_StringBuilder.AppendLine("*** Indent depth exceeded, dump aborted ***");
|
||||
return m_StringBuilder;
|
||||
}
|
||||
|
||||
if (m_StringBuilder.Length > 32 * 1024 * 1024)
|
||||
{
|
||||
m_StringBuilder.AppendLine("*** StringBuilder length exceeded, dump aborted ***");
|
||||
return m_StringBuilder;
|
||||
}
|
||||
|
||||
Indent();
|
||||
|
||||
foreach (FieldInfo field in thingType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
|
||||
{
|
||||
if (typeof(Array).IsAssignableFrom(field.FieldType))
|
||||
{
|
||||
// An array
|
||||
Array array = field.GetValue(thingToDump) as Array;
|
||||
if (array != null)
|
||||
{
|
||||
if (array.Rank != 1)
|
||||
throw new InvalidDataException($"Arrays of ranks other than 1 are not currently supported - array '{field.Name}' is rank {array.Rank})");
|
||||
Type elementType = field.FieldType.GetElementType();
|
||||
Add(field.FieldType.Name, field.Name, $"{DescribeType(elementType)}[{array.Length}]");
|
||||
string name = SanitiseFieldName(field.Name);
|
||||
if (elementType == typeof(string))
|
||||
{
|
||||
Indent();
|
||||
for (int elementIndex = 0; elementIndex < array.Length; elementIndex++)
|
||||
{
|
||||
Add($"{name}[{elementIndex}]", "\"" + ((string)array.GetValue(elementIndex)) + "\"");
|
||||
}
|
||||
Undent();
|
||||
}
|
||||
else if (elementType == typeof(byte))
|
||||
DumpPrimitiveArray<byte>(name, array);
|
||||
else if (elementType == typeof(Type))
|
||||
DumpSimpleObjectArray<Type>(name, array);
|
||||
else
|
||||
{
|
||||
// General array
|
||||
for (int elementIndex = 0; elementIndex < array.Length; elementIndex++)
|
||||
{
|
||||
Indent();
|
||||
object element = array.GetValue(elementIndex);
|
||||
Dump($"{name}[{elementIndex}] ({DescribeType(element.GetType())})", element);
|
||||
Undent();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
Add(field.FieldType.Name, field.Name, "null (array)");
|
||||
}
|
||||
else if (field.FieldType.IsGenericType && (field.FieldType.GetGenericTypeDefinition() == typeof(List<>)))
|
||||
{
|
||||
// A List<>
|
||||
System.Collections.IList list = field.GetValue(thingToDump) as System.Collections.IList;
|
||||
if (list != null)
|
||||
{
|
||||
string typeString = DescribeType(field.FieldType);
|
||||
Add(typeString, field.Name, $"({list.Count})");
|
||||
string name = SanitiseFieldName(field.Name);
|
||||
for (int elementIndex = 0; elementIndex < list.Count; elementIndex++)
|
||||
{
|
||||
Indent();
|
||||
object element = list[elementIndex];
|
||||
Dump($"{name}[{elementIndex}] ({DescribeType(element.GetType())})", element);
|
||||
Undent();
|
||||
}
|
||||
}
|
||||
else
|
||||
Add(field.FieldType.Name, field.Name, "null (List<>)");
|
||||
}
|
||||
else if (field.FieldType.IsEnum)
|
||||
{
|
||||
Add("enum " + field.FieldType.Name, SanitiseFieldName(field.Name), Enum.GetName(field.FieldType, field.GetValue(thingToDump)));
|
||||
}
|
||||
else if (field.FieldType == typeof(String))
|
||||
{
|
||||
object fieldValue = field.GetValue(thingToDump);
|
||||
if (fieldValue != null)
|
||||
Add(field.FieldType.Name, SanitiseFieldName(field.Name), "\"" + ((string)fieldValue) + "\"");
|
||||
else
|
||||
Add(field.FieldType.Name, SanitiseFieldName(field.Name), "<null>");
|
||||
}
|
||||
else if (field.FieldType == typeof(RuntimeTypeHandle))
|
||||
{
|
||||
Add("RuntimeTypeHandle", field.Name, Type.GetTypeFromHandle((RuntimeTypeHandle)field.GetValue(thingToDump)).AssemblyQualifiedName);
|
||||
}
|
||||
else if (field.FieldType.IsClass)
|
||||
{
|
||||
if (String.Equals(field.FieldType.Name, "MonoCMethod")) // Don't recurse into 'MonoCMethod' as it can end up in a loop
|
||||
Add("class " + field.FieldType.Name, SanitiseFieldName(field.Name));
|
||||
else
|
||||
Dump("class " + field.FieldType.Name + " " + SanitiseFieldName(field.Name), field.GetValue(thingToDump));
|
||||
}
|
||||
else if (field.FieldType.IsValueType && (!field.FieldType.IsPrimitive))
|
||||
{
|
||||
Dump("struct " + field.FieldType.Name + " " + SanitiseFieldName(field.Name), field.GetValue(thingToDump));
|
||||
}
|
||||
else
|
||||
Add(field.FieldType.Name, SanitiseFieldName(field.Name), field.GetValue(thingToDump).ToString());
|
||||
}
|
||||
|
||||
Undent();
|
||||
|
||||
return m_StringBuilder;
|
||||
}
|
||||
|
||||
void DumpPrimitiveArray<PrimitiveType>(string name, Array array)
|
||||
{
|
||||
Indent();
|
||||
for (int elementIndex = 0; elementIndex < array.Length; elementIndex++)
|
||||
{
|
||||
Add($"{name}[{elementIndex}]", ((PrimitiveType)array.GetValue(elementIndex)).ToString());
|
||||
}
|
||||
Undent();
|
||||
}
|
||||
|
||||
void DumpSimpleObjectArray<PrimitiveType>(string name, Array array)
|
||||
{
|
||||
Indent();
|
||||
for (int elementIndex = 0; elementIndex < array.Length; elementIndex++)
|
||||
{
|
||||
PrimitiveType element = (PrimitiveType)array.GetValue(elementIndex);
|
||||
if (element != null)
|
||||
Add($"{name}[{elementIndex}]", element.ToString());
|
||||
else
|
||||
Add($"{name}[{elementIndex}]", "null");
|
||||
}
|
||||
Undent();
|
||||
}
|
||||
}
|
||||
|
||||
// Custom DumpToText support for BuildUsageTagSet that cannot be properly dumped by reflection alone
|
||||
class CustomDumper_BuildUsageTagSet : DumpToText.ICustomDumper
|
||||
{
|
||||
Type DumpToText.ICustomDumper.GetType()
|
||||
{
|
||||
return typeof(BuildUsageTagSet);
|
||||
}
|
||||
|
||||
void DumpToText.ICustomDumper.CustomDumper(DumpToText dumpToText, object value)
|
||||
{
|
||||
BuildUsageTagSet buildUsageTagSet = (BuildUsageTagSet)value;
|
||||
ObjectIdentifier[] objectIdentifiers = buildUsageTagSet.GetObjectIdentifiers();
|
||||
dumpToText.Indent();
|
||||
if (objectIdentifiers != null)
|
||||
{
|
||||
dumpToText.Add("ObjectIdentifier[]", "objectIdentifiers", $"ObjectIdentifier[{objectIdentifiers.Length}]");
|
||||
dumpToText.Indent();
|
||||
for (int objectIdentifierIndex = 0; objectIdentifierIndex < objectIdentifiers.Length; objectIdentifierIndex++)
|
||||
{
|
||||
dumpToText.Add("ObjectIdentifier", $"[{objectIdentifierIndex}]");
|
||||
dumpToText.Indent();
|
||||
dumpToText.Dump("ObjectIdentifier", objectIdentifiers[objectIdentifierIndex]);
|
||||
dumpToText.Undent();
|
||||
}
|
||||
dumpToText.Undent();
|
||||
}
|
||||
else
|
||||
dumpToText.Add("ObjectIdentifier[]", "objectIdentifiers", "null");
|
||||
dumpToText.Undent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e00f1ff88f9783243aca917c14f4a913
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,60 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor.SceneManagement;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
internal static class ValidationMethods
|
||||
{
|
||||
internal static System.Func<GUID, Status> ValidAssetFake;
|
||||
|
||||
public enum Status
|
||||
{
|
||||
Invalid,
|
||||
Asset,
|
||||
Scene
|
||||
}
|
||||
|
||||
public static Status ValidAsset(GUID asset)
|
||||
{
|
||||
if (ValidAssetFake != null)
|
||||
return ValidAssetFake(asset);
|
||||
|
||||
var path = AssetDatabase.GUIDToAssetPath(asset.ToString());
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
||||
return Status.Invalid;
|
||||
|
||||
if (path.EndsWith(".unity"))
|
||||
return Status.Scene;
|
||||
|
||||
return Status.Asset;
|
||||
}
|
||||
|
||||
public static bool ValidSceneBundle(List<GUID> assets)
|
||||
{
|
||||
return assets.All(x => ValidAsset(x) == Status.Scene);
|
||||
}
|
||||
|
||||
public static bool ValidAssetBundle(List<GUID> assets)
|
||||
{
|
||||
return assets.All(x => ValidAsset(x) == Status.Asset);
|
||||
}
|
||||
|
||||
public static bool HasDirtyScenes()
|
||||
{
|
||||
var unsavedChanges = false;
|
||||
var sceneCount = EditorSceneManager.sceneCount;
|
||||
for (var i = 0; i < sceneCount; ++i)
|
||||
{
|
||||
var scene = EditorSceneManager.GetSceneAt(i);
|
||||
if (!scene.isDirty)
|
||||
continue;
|
||||
unsavedChanges = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return unsavedChanges;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f2a640ae477de17469deed96101db8c5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,26 @@
|
|||
#if UNITY_2019_4_OR_NEWER
|
||||
using System;
|
||||
|
||||
namespace UnityEditor.Build.Pipeline.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute provides the version details for IProcessScene, IProcessSceneWithReport, IPreprocessShaders, and IPreprocessComputeShaders callbacks.
|
||||
/// Increment the version number when the callback changes and the build result needs to change.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
|
||||
public class VersionedCallbackAttribute : Attribute
|
||||
{
|
||||
public readonly float version;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute provides the version details for IProcessScene, IProcessSceneWithReport, IPreprocessShaders, and IPreprocessComputeShaders callbacks.
|
||||
/// Increment the version number when the callback changes and the build result needs to change.
|
||||
/// </summary>
|
||||
/// <param name="version">The version of this callback.</param>
|
||||
public VersionedCallbackAttribute(float version)
|
||||
{
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ce016609bb1904e4e8ff7bc8d88c0278
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
Add table
Add a link
Reference in a new issue