OvrAvatarTextureCopyManager.cs 9.58 KB
using System.Collections;
using System.Collections.Generic;
using Oculus.Avatar;
using UnityEngine;

public class OvrAvatarTextureCopyManager : MonoBehaviour
{
    [System.Serializable]
    public struct FallbackTextureSet
    {
        public bool Initialized;
        public Texture2D DiffuseRoughness;
        public Texture2D Normal;
    }
    // Fallback texture sets are indexed with ovrAvatarAssetLevelOfDetail.
    // We currently only use 1, 3 (mobile default), 5 (PC default).
    public FallbackTextureSet[] FallbackTextureSets = new FallbackTextureSet[(int)ovrAvatarAssetLevelOfDetail.Highest + 1];

    struct CopyTextureParams
    {
        public Texture Src;
        public Texture Dst;
        public int Mip;
        public int SrcSize;
        public int DstElement;

        public CopyTextureParams(
            Texture src, 
            Texture dst, 
            int mip, 
            int srcSize, 
            int dstElement)
        {
            Src = src;
            Dst = dst;
            Mip = mip;  
            SrcSize = srcSize;
            DstElement = dstElement;
        }
    }
    private Queue<CopyTextureParams> texturesToCopy;

    public struct TextureSet
    {
        // Contains all texture asset IDs that are part of an avatar spec.
        // Used by DeleteTextureSet().
        // Textures that are part of combined mesh avatars can be safely deleted once they have been
        // uploaded to the texture arrays.
        // Textures that are part of single component meshes will remain in memory.
        public Dictionary<ulong, bool> TextureIDSingleMeshPair;
        public bool IsProcessed;

        public TextureSet(
            Dictionary<ulong, bool> textureIDSingleMeshPair,
            bool isProcessed)
        {
            TextureIDSingleMeshPair = textureIDSingleMeshPair;
            IsProcessed = isProcessed;
        }
    }
    private Dictionary<int, TextureSet> textureSets;
    
    private const int TEXTURES_TO_COPY_QUEUE_CAPACITY = 256;
    private const int COPIES_PER_FRAME = 8;

    // Fallback texture paths are indexed with ovrAvatarAssetLevelOfDetail
    // We currently only use 1, 3 (mobile default), 5 (PC default) 
    private readonly string[] FALLBACK_TEXTURE_PATHS_DIFFUSE_ROUGHNESS = new string[]
    {
        "null",
        PATH_LOWEST_DIFFUSE_ROUGHNESS,
        "null",
        PATH_MEDIUM_DIFFUSE_ROUGHNESS,
        "null",
        PATH_HIGHEST_DIFFUSE_ROUGHNESS,
    };
    private readonly string[] FALLBACK_TEXTURE_PATHS_NORMAL = new string[]
    {
        "null",
        PATH_LOWEST_NORMAL,
        "null",
        PATH_MEDIUM_NORMAL,
        "null",
        PATH_HIGHEST_NORMAL,
    };

    private const string PATH_HIGHEST_DIFFUSE_ROUGHNESS = "FallbackTextures/fallback_diffuse_roughness_2048";
    private const string PATH_MEDIUM_DIFFUSE_ROUGHNESS = "FallbackTextures/fallback_diffuse_roughness_1024";
    private const string PATH_LOWEST_DIFFUSE_ROUGHNESS = "FallbackTextures/fallback_diffuse_roughness_256";
    private const string PATH_HIGHEST_NORMAL = "FallbackTextures/fallback_normal_2048";
    private const string PATH_MEDIUM_NORMAL = "FallbackTextures/fallback_normal_1024";
    private const string PATH_LOWEST_NORMAL = "FallbackTextures/fallback_normal_256";

    private const int GPU_TEXTURE_COPY_WAIT_TIME = 10;

    public OvrAvatarTextureCopyManager()
    {
        texturesToCopy = new Queue<CopyTextureParams>(TEXTURES_TO_COPY_QUEUE_CAPACITY);
        textureSets = new Dictionary<int, TextureSet>();
    }

    public void Update()
    {
        if (texturesToCopy.Count == 0)
        {
            return;
        }

        lock (texturesToCopy)
        {
            for (int i = 0; i < Mathf.Min(COPIES_PER_FRAME, texturesToCopy.Count); ++i)
            {
                CopyTexture(texturesToCopy.Dequeue());
            }
        }
    }

    public int GetTextureCount()
    {
        return texturesToCopy.Count;
    }

    public void CopyTexture(
        Texture src,
        Texture dst,
        int mipLevel,
        int mipSize,
        int dstElement,
        bool useQueue = true)
    {
        var copyTextureParams = new CopyTextureParams(src, dst, mipLevel, mipSize, dstElement);

        if (useQueue)
        {
            lock (texturesToCopy)
            {
                if (texturesToCopy.Count < TEXTURES_TO_COPY_QUEUE_CAPACITY)
                {
                    texturesToCopy.Enqueue(copyTextureParams);
                }
                else
                {
                    // Queue is full so copy texture immediately
                    CopyTexture(copyTextureParams);
                }
            }
        }
        else
        {
            CopyTexture(copyTextureParams);
        }
    }

    private void CopyTexture(CopyTextureParams copyTextureParams)
    {
        Graphics.CopyTexture(
            copyTextureParams.Src,
            0,
            copyTextureParams.Mip,
            copyTextureParams.Dst,
            copyTextureParams.DstElement,
            copyTextureParams.Mip);
    }

    public void AddTextureIDToTextureSet(int gameobjectID, ulong textureID, bool isSingleMesh)
    {
        if (!textureSets.ContainsKey(gameobjectID))
        {
            TextureSet newTextureSet = new TextureSet(new Dictionary<ulong, bool>(), false);
            newTextureSet.TextureIDSingleMeshPair.Add(textureID, isSingleMesh);
            textureSets.Add(gameobjectID, newTextureSet);
        }
        else
        {
            bool TexIDSingleMesh;
            if (textureSets[gameobjectID].TextureIDSingleMeshPair.TryGetValue(textureID, out TexIDSingleMesh))
            {
                if (!TexIDSingleMesh && isSingleMesh)
                {
                    textureSets[gameobjectID].TextureIDSingleMeshPair[textureID] = true;
                }
            }
            else
            {
                textureSets[gameobjectID].TextureIDSingleMeshPair.Add(textureID, isSingleMesh);
            }
        }
    }

    // This is called by a fully loaded avatar using combined mesh to safely delete unused textures.
    public void DeleteTextureSet(int gameobjectID)
    {
        TextureSet textureSetToDelete;
        if (!textureSets.TryGetValue(gameobjectID, out textureSetToDelete))
        {
            return;
        };

        if (textureSetToDelete.IsProcessed)
        {
            return;
        }

        StartCoroutine(DeleteTextureSetCoroutine(textureSetToDelete, gameobjectID));
    }

    private IEnumerator DeleteTextureSetCoroutine(TextureSet textureSetToDelete, int gameobjectID)
    {
        // Wait a conservative amount of time for gpu upload to finish. Unity 2017 doesn't support async GPU calls,
        // so this 10 second time is a very conservative delay for this process to occur, which should be <1 sec.
        yield return new WaitForSeconds(GPU_TEXTURE_COPY_WAIT_TIME);

        // Spin if an avatar is loading
        while (OvrAvatarSDKManager.Instance.IsAvatarLoading())
        {
            yield return null;
        }

        // The avatar's texture set is compared against all other loaded or loading avatar texture sets.
        foreach (var textureIdAndSingleMeshFlag in textureSetToDelete.TextureIDSingleMeshPair)
        {
            bool triggerDelete = !textureIdAndSingleMeshFlag.Value;
            if (triggerDelete)
            {
                foreach (KeyValuePair<int, TextureSet> textureSet in textureSets)
                {
                    if (textureSet.Key == gameobjectID)
                    {
                        continue;
                    }

                    foreach (var comparisonTextureIDSingleMeshPair in textureSet.Value.TextureIDSingleMeshPair)
                    {
                        // Mark the texture as not deletable if it's present in another set and that set hasn't been processed
                        // or that texture ID is marked as part of a single mesh component.
                        if (comparisonTextureIDSingleMeshPair.Key == textureIdAndSingleMeshFlag.Key &&
                            (!textureSet.Value.IsProcessed || comparisonTextureIDSingleMeshPair.Value))
                        {
                            triggerDelete = false;
                            break;
                        }
                    }

                    if (!triggerDelete)
                    {
                        break;
                    }
                }
            }

            if (triggerDelete)
            {
                Texture2D textureToDelete = OvrAvatarComponent.GetLoadedTexture(textureIdAndSingleMeshFlag.Key);
                if (textureToDelete != null)
                {
                    AvatarLogger.Log("Deleting texture " + textureIdAndSingleMeshFlag.Key);
                    OvrAvatarSDKManager.Instance.DeleteAssetFromCache(textureIdAndSingleMeshFlag.Key);
                    Destroy(textureToDelete);
                }
            }
        }
        textureSetToDelete.IsProcessed = true;
        textureSets.Remove(gameobjectID);
    }

    public void CheckFallbackTextureSet(ovrAvatarAssetLevelOfDetail lod)
    {
        if (FallbackTextureSets[(int)lod].Initialized)
        {
            return;
        }

        InitFallbackTextureSet(lod);
    }

    private void InitFallbackTextureSet(ovrAvatarAssetLevelOfDetail lod)
    {
        FallbackTextureSets[(int)lod].DiffuseRoughness = FallbackTextureSets[(int)lod].DiffuseRoughness =
            Resources.Load<Texture2D>(FALLBACK_TEXTURE_PATHS_DIFFUSE_ROUGHNESS[(int)lod]);
        FallbackTextureSets[(int)lod].Normal = FallbackTextureSets[(int)lod].Normal =
            Resources.Load<Texture2D>(FALLBACK_TEXTURE_PATHS_NORMAL[(int)lod]);
        FallbackTextureSets[(int)lod].Initialized = true;
    }
}