Search Unity

TextMesh Pro TextMesh Pro: Font assets duplicated in memory (Resources & AssetBundle)

Discussion in 'UGUI & TextMesh Pro' started by Peter77, Apr 5, 2019.

  1. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,609
    TextMesh Pro requires font assets to be placed in a Resources magic folder:
    http://digitalnativestudios.com/textmeshpro/docs/settings/

    We have to use asset bundles in our project. This means assets are pulled-in to asset bundles through Unity's dependencies system.

    This initially caused that a lot of asset bundles pulled in a copy of all TextMesh Pro font assets from the resources folder, wasting a lot of disk and runtime memory.

    In order to avoid that these font assets are stored in more than one asset bundle, I marked the "Resources/Fonts" directory to be stored in a separate asset bundle too.

    This reduced the number of copies of the same font assets to "only two". Two because one copy comes from the asset bundle and the other one from the Resources folder.

    How can I get rid of the second copy in memory? I only want to have TextMesh Pro font assets in memory once.
     
    NyakaBaka and yanivng like this.
  2. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    Are you referencing any of these Font Assets in the text itself using the <font="Name of Font Asset"> tag?

    That is pretty much the only reason why Font Assets need to be contained in a Resources folder.
     
    Peter77 likes this.
  3. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,609
    Thanks for the quick reply. I don't think we use the font-tag yet, but I know that we do use the sprite-tag.

    Have you thought about adding a callback to TextMeshPro, to allow usercode to load a requested TextMesh Pro asset? I believe you could implement that with full backwards compatibility.

    Here is the idea that popped into my head. It's full backwards compatible and it seems it would be a few and simple code changes only to the TMP code.

    In TMP_Settings or wherever you see fit, add a callback where the user can add his/her custom loading method. In order for TextMesh Pro to load resources internally, it does not call Resources.Load() anymore, but TMP_Settings.LoadResource().
    Code (CSharp):
    1. public class TMP_Settings : ScriptableObject
    2. {
    3.     public static System.Func<string, System.Type, UnityEngine.Object> loadResource;
    4.  
    5.     internal static T LoadResource<T>(string assetName) where T : UnityEngine.Object
    6.     {
    7.         T result = default;
    8.  
    9.         if (loadResource != null)
    10.             result = loadResource(assetName, typeof(T)) as T;
    11.  
    12.         if (result == null)
    13.             result = Resources.Load<T>(defaultFontAssetPath + assetName);
    14.  
    15.         return result;
    16.     }
    17. }
    In our game code, we could then do things like:
    Code (CSharp):
    1. [ExecuteInEditMode]
    2. [DefaultExecutionOrder(int.MinValue)]
    3. public class GameTextMeshProAssetManager : MonoBehaviour
    4. {
    5.     void Awake()
    6.     {
    7.         GameObject.DontDestroyOnLoad(gameObject);
    8.     }
    9.  
    10.     void OnEnable()
    11.     {
    12.         TMP_Settings.loadResource += OnLoadTextMeshProResource;
    13.     }
    14.  
    15.     void OnDisable()
    16.     {
    17.         TMP_Settings.loadResource -= OnLoadTextMeshProResource;
    18.     }
    19.  
    20.     UnityEngine.Object OnLoadTextMeshProResource(string assetName, System.Type assetType)
    21.     {
    22. #if UNITY_EDITOR
    23.         if (Application.isEditor)
    24.         {
    25.             string assetPath = "";
    26.  
    27.             if (assetType == typeof(ScriptableObject))
    28.                 assetPath = string.Format("Assets/Fonts/{0}.asset", assetName);
    29.  
    30.             if (string.IsNullOrEmpty(assetPath))
    31.             {
    32.                 string[] guids = UnityEditor.AssetDatabase.FindAssets(string.Format("t:{0} {1}", assetType.Name, assetName));
    33.                 if (guids.Length > 0)
    34.                     assetPath = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[0]);
    35.             }
    36.  
    37.             return UnityEditor.AssetDatabase.LoadAssetAtPath(assetPath, assetType);
    38.         }
    39. #endif
    40.  
    41.         var ab = GetOrLoadAssetBundle("font_assets");
    42.         return ab.LoadAsset(assetName, assetType);
    43.     }
    44. }
    What do you think? Could this be a short-time solution to support loading TextMesh Pro assets from places outside the resources folder?
     
    Last edited: Apr 6, 2019
    yanivng likes this.
  4. yanivng

    yanivng

    Joined:
    May 21, 2017
    Posts:
    43
    @Peter77 This is a great idea.
    Since I don't see @Stephan_B answered, how did you solve the duplication issue?
     
  5. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,609
    I believe I didn't solve it.
     
  6. yanivng

    yanivng

    Joined:
    May 21, 2017
    Posts:
    43
    @Stephan_B I would appreciate your help to solve the font asset duplication issue, as described by @Peter77 above.
     
  7. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    The duplication is typically the result of how the AssetBundle is setup. For instance, if you bundle 2 font assets, each bundle will include the shaders thus resulting in duplication of resources. To avoid this, you would create a separate bundle that only includes the shaders where the other two bundles have a dependency on this shader bundle.

    Can you provide more information / some images showing how your bundle is setup?
     
  8. yanivng

    yanivng

    Joined:
    May 21, 2017
    Posts:
    43
    Hi @Stephan_B ,

    Problem description:
    • TMP Settings asset file is located under Assets\TextMesh Pro\Resources folder, which means it is embedded within the build target.
    • TMP Settings is referencing a Default Font Asset file (Resides in Assets\Fonts and contains a font atlas texture). Due to this reason, the font asset file is also embedded in the build, and loaded into memory on startup.
    • In addition, the same Default Font Asset file in included in an asset bundle. The reason for that, is that lots of prefabs are using this font file and they are a part of an asset bundle, so if this file is not included in an asset bundle it will automatically be added to their asset bundle (and in case of multiple asset bundle, it may be duplicated many times).

    Possible solution, please review the following suggestion:

    • During editor time, TMP Settings will continue to reference the default font asset file (so that new text objects use it).
    • Before building a target version, this reference will be deleted (Can't be done in code, please add support).
    • On application run time, after font asset file is loaded from asset bundle, startup code will set the default font asset file into TMP settings (Is this really needed??? Again support from your side is needed here)

    This way there will only one copy of this asset in memory.
     
  9. yanivng

    yanivng

    Joined:
    May 21, 2017
    Posts:
    43
    @Stephan_B I would love to hear your take on the above...
     
    roi_laredo likes this.
  10. roi_laredo

    roi_laredo

    Joined:
    Sep 12, 2017
    Posts:
    2

    +1.
    waiting to @Stephan_B response
     
  11. yanivng

    yanivng

    Joined:
    May 21, 2017
    Posts:
    43
    @Stephan_B
    I have tried the suggested solution above (manually removing the reference before the build, because it is impossible by script for now) and it works. The duplication of font atlas in memory is gone.
    It would be great if you can add support in code so the default font asset can be assigned and removed via code.

    Thanks!
     
    Last edited: Nov 10, 2019
  12. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    I have been reading and thinking about options.

    Adding the ability to change some of the TMP Settings and assigned resources via scripting is certainly something I can add.
     
    yanivng likes this.
  13. yanivng

    yanivng

    Joined:
    May 21, 2017
    Posts:
    43
    Great!!
    Just to make sure: The reference in TMP Settings to the default font asset is used only for editor time, is that correct?

    When possible, please specify the TMPro version number which allows control over this field using script.
    Thank you!
     
  14. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    It is used in the Editor and at Runtime whenever a new text object is created.
     
  15. yanivng

    yanivng

    Joined:
    May 21, 2017
    Posts:
    43
    So, two alternatives:
    1. As long as we don't use AddComponent<TextMeshProUGUI> during runtime, we are safe.
    2. Alternatively, if we set the default font asset in TMP Settings after it is loaded from asset bundle (once scripting support is in place), we can also use AddComponent<TextMeshProUGUI> at runtime.
     
  16. yanivng

    yanivng

    Joined:
    May 21, 2017
    Posts:
    43
    Hi @Stephan_B ,

    Is this feature available in any preview version of TMPro?

    Thanks,
    Yaniv
     
  17. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    I'll try to add the ability to change these default assets in Preview 3.
     
    yanivng likes this.
  18. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,609
    I'm in the middle of implementing Addressables and this made me aware again that TextMesh Pro relies on the Resources folder.

    Are there any plans to support Addressables in TMP?
     
    chriseborn likes this.
  19. Stephan_B

    Stephan_B

    Joined:
    Feb 26, 2017
    Posts:
    6,595
    No plans right now to support them directly in TMP.

    However, I do plan on adding a TMP Resource Manager where users will be able to implement their own loading of TMP related resources from AssetBundles, Addressables or any other source and then to subsequently add them to the TMP Resource Manager which will enable TMP to use them regardless of how they were loaded or where they came from.
     
    Peter77 likes this.
  20. SagoRich

    SagoRich

    Joined:
    Jul 21, 2014
    Posts:
    15
    Just two copies? Consider yourself lucky! :)

    We have a similar issue, in that we include the font asset (.otf, for a dynamic font) in an AssetBundle so that it will not be duplicated into multiple ABs. But it is. Despite being included in, say, "project_fonts", it is getting included in every other AB that uses the TMP Font Asset. Since the particular font in question is for languages with larger character sets, it is huge (11MB), so our build is over 55MB bigger for no good reason that I can see, and as we add more content, it will just be duplicated more times.

    I have created a separate, tiny, project to illustrate the problem. I have reproduced it in the latest 2019 and 2018 LTS builds, and submitted a bug (Case 1228188). @Stephan_B it would be great if you could take a quick look to at least ensure it is appropriately triaged.
     
    Stephan_B and Peter77 like this.
  21. SagoRich

    SagoRich

    Joined:
    Jul 21, 2014
    Posts:
    15
  22. SagoRich

    SagoRich

    Joined:
    Jul 21, 2014
    Posts:
    15
    Update:

    Our particular issue (multiple copies of assets in AssetBundles) seems to have been due to an incorrect interpretation of the behaviour of BuildPipeline.BuildAssetBundles. If asked to build all AssetBundles (the first version of the function), the assets are correctly handled (e.g. only one copy of an asset across all bundles). If asked to build a subset of AssetBundles (the second version of the function) the API ignores AssetBundle settings in the project, and just inhales every dependency that is not in the same build group.

    This makes some sense (and is technically correct, since I assume under the hood one function just calls the other), because it protects you from messing up by missing something, but it is definitely not intuitive and could be spelled out more clearly in the docs. It means that you cannot, say, rebuild a single AssetBundle without building all AssetBundles, unless you're ok with the one built alone ballooning in size (and presumably masking the "real" assets in other AssetBundles).

    The OP issue of having two copies (one in Resources, and one in AssetBundles) still stands; apologies for the diversion.
     
    NyakaBaka and Peter77 like this.
  23. icaro56

    icaro56

    Joined:
    Apr 4, 2018
    Posts:
    6
    There is still no solution today?
     
    chriseborn and lovor like this.