368 lines
16 KiB
C#
368 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using Unity.Profiling;
|
|
using UnityEngine;
|
|
using VRC.Compression;
|
|
using VRC.SDK3.UdonNetworkCalling;
|
|
using VRC.SDKBase;
|
|
using VRC.Udon.Common;
|
|
using VRC.Udon.Common.Interfaces;
|
|
using VRC.Udon.Security;
|
|
using VRC.Udon.Serialization.OdinSerializer;
|
|
using VRC.Udon.Serialization.OdinSerializer.Utilities;
|
|
|
|
[assembly: UdonSignatureHolderMarker(typeof(VRC.Udon.ProgramSources.SerializedUdonProgramAsset))]
|
|
|
|
namespace VRC.Udon.ProgramSources
|
|
{
|
|
public sealed class SerializedUdonProgramAsset : AbstractSerializedUdonProgramAsset, IUdonSignatureHolder
|
|
{
|
|
private static readonly Lazy<string> _debugCategory = new Lazy<string>(InitializeLogging);
|
|
private static string DebugCategoryName => _debugCategory.Value;
|
|
|
|
private const DataFormat DEFAULT_SERIALIZATION_DATA_FORMAT = DataFormat.Binary;
|
|
private const int MAXIMUM_CACHED_PROGRAM_SIZE = 1024 * 1024 * 2; // 2 MB
|
|
|
|
[SerializeField, HideInInspector]
|
|
private byte[] serializedProgramCompressedBytes;
|
|
|
|
[SerializeField, HideInInspector]
|
|
private string serializedProgramBytesString;
|
|
|
|
[SerializeField, HideInInspector]
|
|
private byte[] serializedSignature;
|
|
|
|
[SerializeField, HideInInspector]
|
|
private List<UnityEngine.Object> programUnityEngineObjects;
|
|
|
|
[SerializeField, HideInInspector]
|
|
private NetworkCallingEntrypointMetadata[] networkCallingEntrypointMetadata;
|
|
|
|
public override NetworkCallingEntrypointMetadata[] GetNetworkCallingMetadata() => networkCallingEntrypointMetadata;
|
|
|
|
[NonSerialized] private Dictionary<string, NetworkCallingEntrypointMetadata> _networkCallingEntrypointMetadataMap;
|
|
public override NetworkCallingEntrypointMetadata GetNetworkCallingMetadata(string entrypoint)
|
|
{
|
|
if (networkCallingEntrypointMetadata == null)
|
|
return null;
|
|
if (_networkCallingEntrypointMetadataMap == null)
|
|
{
|
|
_networkCallingEntrypointMetadataMap = new();
|
|
foreach (var metadata in networkCallingEntrypointMetadata)
|
|
{
|
|
if (metadata != null)
|
|
_networkCallingEntrypointMetadataMap[metadata.Name] = metadata;
|
|
}
|
|
}
|
|
return _networkCallingEntrypointMetadataMap.TryGetValue(entrypoint, out var result) ? result : null;
|
|
}
|
|
|
|
// Store the serialization DataFormat that was actually used to serialize the program.
|
|
// This allows us to change the DataFormat later (ex. switch to binary) without causing already serialized programs to use the wrong DataFormat.
|
|
// Programs will be deserialized using the previous format and will switch to the new format if StoreProgram is called again later.
|
|
[SerializeField, HideInInspector]
|
|
private DataFormat serializationDataFormat = DEFAULT_SERIALIZATION_DATA_FORMAT;
|
|
|
|
// Cache the deserialized program and a serialized copy of its IUdonHeap to more efficiently create clones of the IUdonProgram.
|
|
[NonSerialized] private (IUdonProgram program, byte[] serializedHeap, List<UnityEngine.Object> serializedHeapUnityEngineObjects)? _serializationCache = null;
|
|
|
|
[NonSerialized] private int _mainThreadId;
|
|
|
|
private void OnEnable()
|
|
{
|
|
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
|
|
#if VRC_CLIENT
|
|
try
|
|
{
|
|
ulong totalProgramSize = GetSerializedProgramSize();
|
|
if(totalProgramSize >= MAXIMUM_CACHED_PROGRAM_SIZE)
|
|
{
|
|
Core.Logger.LogWarning(
|
|
$"Skipping caching of UdonProgram '{name}' as the total program size ({totalProgramSize}) is higher than '{MAXIMUM_CACHED_PROGRAM_SIZE}'",
|
|
DebugCategoryName);
|
|
|
|
_serializationCache = null;
|
|
return;
|
|
}
|
|
|
|
// Deserialize the full program once and cache it.
|
|
// Then reserialize the IUdonHeap and cache it so we can deserialize a clone for each UdonProgram.
|
|
// This is more efficient than SerializationUtility.CreateCopy which serializes each time.
|
|
IUdonProgram deserializedUdonProgram = ReadSerializedProgram();
|
|
if(deserializedUdonProgram == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
byte[] serializedUdonHeap = SerializationUtility.SerializeValue(
|
|
deserializedUdonProgram.Heap,
|
|
serializationDataFormat,
|
|
out List<UnityEngine.Object> serializedHeapUnityEngineObjects);
|
|
|
|
_serializationCache = (deserializedUdonProgram, serializedUdonHeap, serializedHeapUnityEngineObjects);
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
Core.Logger.LogWarning($"Failed to deserialize Udon Program due to :\n{e}.", DebugCategoryName);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse")]
|
|
[SuppressMessage("ReSharper", "HeuristicUnreachableCode")]
|
|
public override void StoreProgram(IUdonProgram udonProgram)
|
|
{
|
|
if(this == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
byte[] serializedProgramBytes = SerializationUtility.SerializeValue(udonProgram, DEFAULT_SERIALIZATION_DATA_FORMAT, out programUnityEngineObjects);
|
|
// Store a compressed byte array only - we no longer store Base64 encoded strings.
|
|
serializedProgramCompressedBytes = GZip.Compress(serializedProgramBytes);
|
|
serializedProgramBytesString = string.Empty;
|
|
serializationDataFormat = DEFAULT_SERIALIZATION_DATA_FORMAT;
|
|
networkCallingEntrypointMetadata = null;
|
|
|
|
#if UNITY_EDITOR
|
|
UnityEditor.EditorUtility.SetDirty(this);
|
|
#endif
|
|
}
|
|
|
|
public override void StoreProgram(IUdonProgram udonProgram, NetworkCallingEntrypointMetadata[] networkCallingMetadata)
|
|
{
|
|
StoreProgram(udonProgram);
|
|
networkCallingEntrypointMetadata = networkCallingMetadata;
|
|
}
|
|
|
|
private ProfilerMarker _retrieveProgramProfilerMarker = new ProfilerMarker("SerializedUdonProgram.RetrieveProgram");
|
|
private ProfilerMarker _retrieveProgramCopyHeapProfilerMarker = new ProfilerMarker("SerializedUdonProgram.RetrieveProgram CopyHeap");
|
|
private ProfilerMarker _cloneProgramCopyByteCodeProfilerMarker = new ProfilerMarker("SerializedUdonProgram.RetrieveProgram CopyByteCode");
|
|
|
|
public override IUdonProgram RetrieveProgram()
|
|
{
|
|
using(_retrieveProgramProfilerMarker.Auto())
|
|
{
|
|
try
|
|
{
|
|
if(this == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if(_serializationCache == null)
|
|
{
|
|
var deserializedProgram = ReadSerializedProgram();
|
|
PopulateEntrypointHashes(deserializedProgram);
|
|
return deserializedProgram;
|
|
}
|
|
|
|
(IUdonProgram program, byte[] serializedHeap, List<UnityEngine.Object> serializedHeapUnityEngineObjects) = _serializationCache.Value;
|
|
if(program == null || serializedHeap == null || serializedHeapUnityEngineObjects == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Clone the byte code array.
|
|
byte[] byteCodeCopy;
|
|
using(_cloneProgramCopyByteCodeProfilerMarker.Auto())
|
|
{
|
|
byte[] byteCode = program.ByteCode;
|
|
byteCodeCopy = new byte[byteCode.Length];
|
|
Array.Copy(byteCode, byteCodeCopy, byteCode.Length);
|
|
}
|
|
|
|
// Deserialize a fresh copy of the serialized IUdonHeap from earlier.
|
|
IUdonHeap udonHeapCopy;
|
|
using(_retrieveProgramCopyHeapProfilerMarker.Auto())
|
|
{
|
|
using (var cachedContext = Cache<DeserializationContext>.Claim())
|
|
{
|
|
var context = cachedContext.Value;
|
|
context.Config.SerializationPolicy = SerializationPolicies.Everything;
|
|
context.Config.DebugContext.ErrorHandlingPolicy =
|
|
Thread.CurrentThread.ManagedThreadId == _mainThreadId
|
|
? ErrorHandlingPolicy.Resilient
|
|
: ErrorHandlingPolicy.ThrowOnWarningsAndErrors;
|
|
udonHeapCopy = SerializationUtility.DeserializeValue<IUdonHeap>(
|
|
serializedHeap,
|
|
serializationDataFormat,
|
|
serializedHeapUnityEngineObjects,
|
|
context);
|
|
}
|
|
}
|
|
|
|
// Everything except the byte code array and IUdonHeap are immutable so they don't need to be cloned.
|
|
var clonedProgram = new UdonProgram(
|
|
program.InstructionSetIdentifier,
|
|
program.InstructionSetVersion,
|
|
byteCodeCopy,
|
|
udonHeapCopy,
|
|
program.EntryPoints,
|
|
program.SymbolTable,
|
|
program.SyncMetadataTable,
|
|
program.UpdateOrder
|
|
);
|
|
PopulateEntrypointHashes(clonedProgram);
|
|
return clonedProgram;
|
|
}
|
|
catch(SerializationAbortException e)
|
|
{
|
|
// Odin can't deserialize Unity Gradients properly off the main-thread
|
|
if(!e.Message.StartsWith("Failed to read Gradient.mode, due to Unity's API"))
|
|
{
|
|
Core.Logger.LogWarning($"Failed to deserialize Udon Program due to an exception:\n{e}.", DebugCategoryName);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
Core.Logger.LogWarning($"Failed to deserialize Udon Program due to an exception:\n{e}.", DebugCategoryName);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private IUdonProgram ReadSerializedProgram()
|
|
{
|
|
byte[] serializedProgramBytes = null;
|
|
|
|
// If the newer compressed bytes format is available, use that.
|
|
if (serializedProgramCompressedBytes != null && serializedProgramCompressedBytes.Length > 0)
|
|
{
|
|
try
|
|
{
|
|
serializedProgramBytes = GZip.Decompress(serializedProgramCompressedBytes);
|
|
}
|
|
catch (InvalidDataException invalidDataException)
|
|
{
|
|
Core.Logger.LogWarning($"Failed to deserialize UdonProgram because the program was invalid. Exception:\n{invalidDataException}");
|
|
}
|
|
}
|
|
|
|
// If there is no compressed byte array or reading the array failed, try to fall back to the base 64 encoded string.
|
|
if (serializedProgramBytes == null)
|
|
{
|
|
try
|
|
{
|
|
serializedProgramBytes = Convert.FromBase64String(serializedProgramBytesString ?? "");
|
|
}
|
|
catch (FormatException formatException)
|
|
{
|
|
Core.Logger.LogWarning($"Failed to deserialize UdonProgram because the program was invalid. Exception:\n{formatException}");
|
|
}
|
|
}
|
|
|
|
if (serializedProgramBytes == null)
|
|
{
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
return SerializationUtility.DeserializeValue<IUdonProgram>(serializedProgramBytes, serializationDataFormat, programUnityEngineObjects);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the total size of this serialized Udon program.
|
|
/// </summary>
|
|
/// <returns>The size of the program in bytes.</returns>
|
|
public override ulong GetSerializedProgramSize()
|
|
{
|
|
if (serializedProgramCompressedBytes != null && serializedProgramCompressedBytes.Length > 0)
|
|
{
|
|
return (ulong)serializedProgramCompressedBytes.Length;
|
|
}
|
|
else if (!string.IsNullOrEmpty(serializedProgramBytesString))
|
|
{
|
|
return (ulong)serializedProgramBytesString.Length;
|
|
}
|
|
|
|
return 0L;
|
|
}
|
|
|
|
private static string InitializeLogging()
|
|
{
|
|
const string categoryName = "SerializedUdonProgramAsset";
|
|
if(Core.Logger.CategoryIsDescribed(categoryName))
|
|
{
|
|
return categoryName;
|
|
}
|
|
|
|
Core.Logger.DescribeCategory(categoryName, Core.Logger.Color.blue);
|
|
Core.Logger.EnableCategory(categoryName);
|
|
return categoryName;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
#if VRC_CLIENT
|
|
serializedProgramCompressedBytes = null;
|
|
serializedProgramBytesString = null;
|
|
programUnityEngineObjects = null;
|
|
#endif
|
|
_serializationCache = null;
|
|
}
|
|
|
|
#region IUdonSignatureHolder
|
|
void IUdonSignatureHolder.EnsureGZipFormat()
|
|
{
|
|
if ((serializedProgramCompressedBytes == null || serializedProgramCompressedBytes.Length == 0) && !string.IsNullOrEmpty(serializedProgramBytesString))
|
|
{
|
|
Core.Logger.Log($"Converting SerializedUdonProgramAsset '{name}' to compressed format");
|
|
serializedProgramCompressedBytes = GZip.Compress(Convert.FromBase64String(serializedProgramBytesString));
|
|
}
|
|
serializedProgramBytesString = null; // always clear, compressedBytes format has priority in case somehow both get set
|
|
}
|
|
|
|
byte[] IUdonSignatureHolder.Signature
|
|
{
|
|
get => serializedSignature;
|
|
set => serializedSignature = value;
|
|
}
|
|
|
|
byte[] IUdonSignatureHolder.SignedData => serializedProgramCompressedBytes;
|
|
|
|
// in client only, allow skipping signature validation for internal behaviours (like stations)
|
|
[field: NonSerialized] public bool IsInternallyValidated { get; private set; } = false;
|
|
#if VRC_CLIENT
|
|
public void SetInternallyValidated() => IsInternallyValidated = true;
|
|
#endif
|
|
#endregion
|
|
|
|
#region Entrypoint Hashing
|
|
|
|
[NonSerialized] private int _entrypointHashesLoaded = 0;
|
|
private readonly Dictionary<uint, string> _entrypointHashToName = new Dictionary<uint, string>();
|
|
private readonly Dictionary<string, uint> _entrypointNameToHash = new Dictionary<string, uint>();
|
|
|
|
private void PopulateEntrypointHashes(IUdonProgram program)
|
|
{
|
|
if (program == null || program.EntryPoints == null)
|
|
return;
|
|
|
|
if (Interlocked.CompareExchange(ref _entrypointHashesLoaded, 1, 0) == 1)
|
|
return;
|
|
|
|
foreach (string entrypoint in program.EntryPoints.GetExportedSymbols())
|
|
{
|
|
// hash with basic collision avoidance, this is why you must use TryGetEntrypointHashFromName instead of Fletcher32Fast directly
|
|
uint hash = Utilities.Fletcher32Fast(entrypoint);
|
|
while (_entrypointHashToName.ContainsKey(hash))
|
|
unchecked { hash++; }
|
|
_entrypointHashToName[hash] = entrypoint;
|
|
_entrypointNameToHash[entrypoint] = hash;
|
|
}
|
|
}
|
|
|
|
public override bool TryGetEntrypointNameFromHash(uint hash, out string entrypoint) => _entrypointHashToName.TryGetValue(hash, out entrypoint);
|
|
public override bool TryGetEntrypointHashFromName(string entrypoint, out uint hash) => _entrypointNameToHash.TryGetValue(entrypoint, out hash);
|
|
|
|
#endregion
|
|
}
|
|
}
|