Jump to content

To other Modders: kOS font stuff might break other mods - can we find a solution together?


Recommended Posts

I'm coming across what I think is a bug in Unity's font system that makes it hard for me to "be nice" to other mods and not break them.

This problem is weird and what I've learned so far is from a few weeks of on/off trial and error and experimentation.  I could be wrong about the cause, but I've barked up a lot of wrong trees already trying to find other possible causes before settling on what I'm about to describe below as what I think is the cause of it.

So what's the problem?

These two Unity methods

Font.GetOSInstalledFontNames
Font.CreateDynamicFontFromOSFont

can break fonts that are also in a Resource/Asset file if these steps happen in this order:

1: Unity loads a font from Resource or Asset files, but hasn't had any occasion to draw anything in that font yet.

2: Using Font.CreateDynamicFontFromOSFont(), You create another Font instance that is for a font the same font family as the one from step 1 above (i.e. loading "Arial bold" when "Arial" was loaded in step 1.)

3: The font from step 2 (DynamicFontFromOSFont) gets rendered into some text.

4: The font from step 1 (From the Resources or Asset file) gets rendered into some text.

When you do the steps in that order, then Unity gets confused and seems to wipe out all the glyphs of BOTH instances of that font from then on (i.e both the one from the Resources and the one from the OS).  From now on it will render all text in that font as blank, (and it now claims that all text drawn in that font is 0 pixels wide and 0 pixels high, so things like GUILayout buttons get shrunken to minimum size in addition to not being able to show the labels on things because the font is blank.)

Note that if you swap steps 3 and 4 so the Resources font gets exercised in some way before the DynamicFontFromOSFont does, the bug does not happen! It only happens when the first attempt to draw something in the font instance that was built from the OS font call happens prior to the first attempt to draw something in the Resources instance of that font.  Note that it's the order in which the fonts get USED to draw something that matters here, not the order in which they first get loaded.  (i.e. you can swap steps 1 and 2 and it doesn't change the outcome).

As you can tell from the fact that I used "Arial" as my example case above, this means when we do this in kOS, I have the chance to break every other mod that uses Unity's default GUI.skin for something.

Oh, and this isn't just about using the legacy IMGUI.  I noticed that the act of using the font *anywhere* in Unity is affected, even when I draw 3D hovering text in Arial in the game scene - if the Arial font has had this bug trigger, then that 3D text won't show up.  I can trigger the bug by choosing to render font text into a Texture2D in memory that I don't even show on screen anywhere.  Even rendering it that way triggers the same problem so long as I do it in the order shown above.

Why did I want to do this?:

 

At this point, the person reading this might be thinking, "Well then just don't do it!  Stop using the OS fonts and instead ship with one and hardcode it.")  So I feel I have to defend my desire to support doing this:

I'm trying to let the user use any font on their OS as the kOS terminal font, and move away from our current technique of cutting and pasting regions from a texture file that contains images of the 128 ASCII chars.  (For 2 reasons: Using a real font scales a lot better than stretching a bitmap image for those users who prefer the terminal to use a bigger font, and more importantly it would let you print to the terminal in your preferred language, for which you probably already have a font you like installed on your computer that's better for that purpose than whatever we might ship with.

But wait, isn't it only a conflict when you actually try to RENDER the font?  Isn't the user just picking one font, not every font on the OS?

True, but Unity does not expose any of the metadata about a font until after you load it, and even then you still have to actually render a few characters with it before all that you need to know manifests itself.  If you haven't loaded a font from the OS yet, then the font's string name is literally the only thing you know about it.  You don't know if it's bold, italic, etc (except from making a heuristic guess from looking for substrings in the font's name like "this font's name has the word 'bold' in it.  I guess it must be a bold font then.".  Most importantly for my case - you can't tell if it's monospace or proportional until after you load it and try rendering a few characters with it.  The font metadata isn't available through Unity.  So I was doing a quick dummy render of a short string containing some wide and some narrow characters, and counting the pixels Unity reported it took to do so to find out if it's monospaced or not.  This is relevant since I use the font to paint a terminal very fast by drawing each line of the terminal as a single string - I need to restrict the picks the user is allowed to the monospace fonts only.  It's that test for monospace that mandates that I actually give each font an experimental test render, and it's doing that which caused me to trigger the bug this post is talking about.  I thought this would be really slow at first (test render every font) but it turns out that even on a computer with a few thousand fonts installed it only takes a couple of seconds, and I only have to do it once and then never again (and I can throw away the font after I tested it so it's not eating up memory once I learned it's proportional).

So why not just avoid it by forcing the order to come out the "safe" way?

 

An obvious fix presents itself:  Before trying to use any Font that comes from CreateDynamicFontFromOSFont, kOS could just make sure to iterate over every Font object that ii finds in the Resources and perform a dummy rendering with each of them.  (i.e. Tell it to render "Hello" into a Texture2D, then throw away the Texture2D, just to exercise the font a bit first which seems to prevent the bug.)

I have tried that and it does work.... but... read on:

I'm not in control of the order that OTHER mods do things in, nor am I in control of what order Unity chooses to call the Awake() and Start() methods of all the Monobehaviours from all the mods, nor am I in control of whether or not other mods might try to wait and lazy-load a font dynamically from an asset bundle later on during the game.  This means there is no point in time when I can reliably answer "yes" to the question: "At this point have all the fonts that will ever get loaded, during the life of this process, from any Resource/Asset, been loaded and we know there will be no more?"  In order to reliably use this workaround to fix the problem, I have to do so at a point in time when that is true, otherwise there will be a Resources/Asset font I missed when I performed the "foreach Resource font, render something small with it" code.

 

So now to the questions for other modders:

(1) How many mods actually bother trying to ship with their own font?  Then again, with SQUAD doing localizations in the next release, who knows if maybe even THEY might wait to load a font later on after game initialization so I can't rely on knowing if they will do so.  Could it be so few mods that the solution is to simply see if we happen to break another mod and if so then react to that and work with the other modder to come up with a scheme to force a known loading order between the fonts used by our two mods?

(2) Do I need to consider splitting this work off into a standalone font manager mod and then make kOS require it as a dependency? Then any modder that wants to load fonts should have to work through it instead of doing it on their own?  (i.e. similar to other library-mods like the CommunityResourcePack, the goal of such a mod would be to make sure all font loading happens in one place where the order can be enforced to prevent the bug.)

(3) Any suggestions for a workaround that I might not have tried?  I'm really not a Unity expert at all.  The only things I know about it I know from doing kOS dev work.  Yes, I am aware of the fact that Unity lazy-loads font glyphs (I found that out when trying to implement other parts of this system) and therefore the need to use Font.RequestCharactersInTexture() before attempting a test render to look at character size.  But I suspect the bug above is somehow related to this lazy-loading feature misfiring in some way so the two different instances of the same-named font are stepping on each other's toes, or maybe Unity is getting fooled into thinking it already performed all the lazy-load work for both versions of the similarly named font when it really only did so for one of them.  (Thus the font's data never gets populated because it thought it already did so?)

(4) As KSP gets more international users, will more mods start considering using their own fonts so that even though this might not be a problem today it will become one soon so I still have to worry about it?

(5) Is this a known Unity bug that was already fixed in a release of Unity but we don't have it yet because KSP is a few revisions behind?  If so might the problem magically fix itself in the next KSP release?  I tried searching Unity's issue tracker for font-related bugs and spent a long time walking through them and not finding anything that seemed related, before I gave up on trying to do that.

Edited by Steven Mading
Link to comment
Share on other sites

Yikes, that's a lot of text that I can't fully parse.

Unfortunately, you are way beyond where I am with fonts and so can't actually help on the issue.

However I want to cast my vote as a modder who will not be including any font files.

I have not looked at it yet personally, but my assumption is that by using the default GUI skin, that inherits whatever font is needed to correctly display the currently selected language is simply available for me to use without having to mess around with my own font (or font files).

I would imagine most mods are going to deal with it this way as well, without kOS's terminal window that has prompted all this, it is worth the developer's time to mess around with fonts? I would expect the answer to be no for most mods, but there might be some that answer yes. (Which causes this whole issue.)

My only though is somehow hooking into Font.CreateDynamicFontFromOSFont() somehow so you can render the previously loaded fonts in the same font family as a work-around, but I don't think you can do that? (Although you've shown me some pretty clever tricks before so I'm still tossing the idea out.)

D.

Link to comment
Share on other sites

4 hours ago, Steven Mading said:

As you can tell from the fact that I used "Arial" as my example case above, this means when we do this in kOS, I have the chance to break every other mod that uses Unity's default GUI.skin for something.

Something sounds fishy here. Are you editing the default GUI.skin? Why?

I couldn't reproduce the problem with the following:

abstract class TestTextRenderer
{
    private Rect _rect = new Rect(0f, 0f, 300f, 300f);
    private GUISkin _skin;
    private string _title;
    private string _text;

    public virtual void Initialize()
    {
        _skin = GetSkin();
        _title = GetTitle();
        _rect.center = new Vector2(Screen.width * 0.5f, Screen.height * 0.5f);
        _text = Enumerable.Range(0, 255).Select(idx => ((char)idx).ToString()).Aggregate((s1, s2) => s1 + s2);
        _text += _text += _text; // increase length a bit

        Log.Normal(_title + " font: " + _skin.font.name + ": " + string.Join(",", _skin.font.fontNames));
    }

    protected abstract GUISkin GetSkin();
    protected abstract string GetTitle();

    public void DrawWindow()
    {
        GUI.skin = _skin;
        _rect = KSPUtil.ClampRectToScreen(GUILayout.Window(_title.GetHashCode(), _rect, DoWindow, _title));
    }

    private void DoWindow(int winid)
    {
        GUILayout.TextArea(_text);
        GUI.DragWindow();
    }
}

class DynamicTextRenderer : TestTextRenderer
{
    private const string FontName = "Arial Bold";
    private const int FontSize = 16;

    protected override GUISkin GetSkin()
    {
        var cloneSkin = UnityEngine.Object.Instantiate(HighLogic.Skin);

        var dynFont = Font.CreateDynamicFontFromOSFont(FontName, FontSize);

        cloneSkin.font = dynFont;

        return cloneSkin;
    }

    protected override string GetTitle()
    {
        return "Dynamic Text";
    }
}

class ResourceTextRenderer : TestTextRenderer
{
    private readonly Font[] _resourceFonts;
    private const string FontName = "Arial";

    public ResourceTextRenderer([NotNull] Font[] resourceFonts)
    {
        if (resourceFonts == null) throw new ArgumentNullException("resourceFonts");
        _resourceFonts = resourceFonts;
    }

    protected override GUISkin GetSkin()
    {
        var cloneSkin = UnityEngine.Object.Instantiate(HighLogic.Skin);

        cloneSkin.font = _resourceFonts.First(f => f.fontNames.Contains(FontName));

        return cloneSkin;
    }

    protected override string GetTitle()
    {
        return "Resource-loaded Font";
    }
}

// controls ordering of text initialization and rendering
//  1. init resource font
//  2. init dyn font
//  3. make sure dyn font renders first
//  4. res font renders second
[KSPAddon(KSPAddon.Startup.MainMenu, true)]
public class RenderOrderController : MonoBehaviour
{
    private DynamicTextRenderer _dynText = null;
    private ResourceTextRenderer _resText = null;

    private void Awake()
    {
        Font.textureRebuilt += FontOnTextureRebuilt;

        _resText = new ResourceTextRenderer(Resources.FindObjectsOfTypeAll<Font>());
        _resText.Initialize();

        _dynText = new DynamicTextRenderer();
        _dynText.Initialize();
    }

    private void OnDestroy()
    {
        Font.textureRebuilt -= FontOnTextureRebuilt;
    }

    private void FontOnTextureRebuilt(Font font)
    {
        Log.Warning("Font rebuilt: {0}; {1}", font.name, string.Join(",", font.fontNames));
    }

    private void OnGUI()
    {
        var origSkin = GUI.skin;

        _dynText.DrawWindow();
        GUI.skin = origSkin;

        _resText.DrawWindow();
    }
}

 

Link to comment
Share on other sites

@xEvilReeperx:

This comment doesn't appear to match the code.

//  3. make sure dyn font renders first
//  4. res font renders second

In the code you pasted, the order is this way around:

        _resText = new ResourceTextRenderer(Resources.FindObjectsOfTypeAll<Font>());
        _resText.Initialize();

        _dynText = new DynamicTextRenderer();
        _dynText.Initialize();

Also, I don't think I'm editing the GUI skin.  It seems once the problem triggers, it's the font itself that breaks, given that it causes attempts to perform a render to an in-memory texture2D (outside the GUI system) to also render as zero-width, zero-height glyphs.

Edited by Steven Mading
Link to comment
Share on other sites

9 hours ago, Diazo said:

I would imagine most mods are going to deal with it this way as well, without kOS's terminal window that has prompted all this, it is worth the developer's time to mess around with fonts? I would expect the answer to be no for most mods, but there might be some that answer yes. (Which causes this whole issue.

My only though is somehow hooking into Font.CreateDynamicFontFromOSFont() somehow so you can render the previously loaded fonts in the same font family as a work-around, but I don't think you can do that? (Although you've shown me some pretty clever tricks before so I'm still tossing the idea out.)

D.

I can see the value in limiting font choices in an application.  A consistent looking interface is handy.  (i.e. choose a few fonts that cover your needs and re-use them everywhere instead of having a new font for each thing the user sees.)  What made this case different was kOS's need for a monospace font (all of KSP's shippped fonts are proportional), and my desire to let people use their native language if they like, and I'm not qualified to pick a good font for each language.  (And even though yes, I'm trying to let the user pick their own font, I'm not trying to let them display ten different fonts at once.  I'm letting the user pick one font that all of kOS will use.)

If I hadn't come across the need for a monospace font, I'd probably just be using KSP's default skin for everything too.  For every GUI widget it draws that's NOT the terminal's screen area, kOS is just using the fonts of HighLogic.skin or GUI.skin, like I imagine every other modder probably is.

As for "render the previously loaded fonts in the same font family", I sort of covered my thoughts on that in my wall of text up above, but here's the in-a-nutshell version of what I said up there:  (1) Unity doesn't tell me what family a font is in.  That information isn't exposed to Unity Programs so I'd have to perform a heuristic guess from examining substrings of the font's name, and that sounds very fragile and error-prone to me. (2) I'd need a guaranteed point in time when I know no other mods will be loading any more fonts, and wait for that to happen before I attempt to do the pre-rendering of Resource fonts, otherwise I'll miss some that aren't there yet.  Given  that a mod is allowed to open an asset file at runtime whenever it feels like, there is no such guaranteed time.

Link to comment
Share on other sites

10 minutes ago, xEvilReeperx said:

@Steven Mading 3 and 4 are for rendering done in OnGUI. Those lines are for 1 and 2 to match the order of initialization you specified in the OP

Okay, that's right.  It's hard to trace your code's order of execution.

Is this a test you performed with this code and this code *only*, as the only existing mod in KSP, or was this added to an installation that had other mods too?  (It matters because if *any* other mod uses the Arial font for something first, then that would prevent the bug from manifesting, if I'm right about my guess as to the trigger conditions.) I will try your test case and see if I can reproduce your stated behavior that it works okay, but I don't see anything you're doing differently from what I'm doing in the more complex full code of kOS.  I'll work out whether I can trigger the bug with the minimal case possible.  Right now we have a complex case that triggers it (the kOS mod in development in my branch) and a simple case that does not (your code example).  I need to start slowly changing your example one thing at a time until I hit the change that triggers it, to help narrow down the problem.

Link to comment
Share on other sites

3 minutes ago, xEvilReeperx said:

That mod only. Yes I knew it's pretty ugly code, it was just slapped together to check things out

No need to apologise. I appreciate the help.  I was more apologising for failing to follow it properly.  If your example does NOT trigger the bug and my code DOES then at least that gives me something to start from to find a minimum case that triggers it.  It's a matter of making your example more complex slowly, one thing at a time, adding the stuff my code does differently, until I hit a point where the bug starts happening.

 

 

Link to comment
Share on other sites

1 hour ago, xEvilReeperx said:

That mod only. Yes I knew it's pretty ugly code, it was just slapped together to check things out

Okay, I edited your example to make it trigger the bug reliably for me (at least on my computer - your mileage may vary).

The change I had to do was to add the test I'm doing for monospaced-ness of the fonts on the computer.  This test involves the use of Font.RequestCharactersInTexture() and Font.GetCharacterInfo().

This is the "attempt to render something in dynamic fonts" that seems to trigger the bug, but only if it happens *before* the first use of the Resources-built-in Arial font.  If you fiddle with the ordering of my millisecond timestamps in the code pasted below to make it so the first _resText.DrawWindow() happens *prior* to the first time I try my monospace-test loop across all fonts, then it doesn't trigger the bug.

(It's explained in the comments in the code pasted below).

Also, notice that my edited version loops over every dynamic OS font and tries to get the character info about all of them in the little monospace test.  I tried to reduce my example a bit by JUST testing for the Dynamic OS Arial font *only* rather than all of the fonts, and that didn't trigger the bug anymore.  I haven't worked out yet *which* of the fonts is actually triggering the problem when I try to dynamically query some CharacterInfo from it, but just doing "Arial" only doesn't trigger it.

using System;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;

abstract class TestTextRenderer
{
    private Rect _rect = new Rect(0f, 0f, 300f, 300f);
    private GUISkin _skin;
    private string _title;
    private string _text;

    public virtual void Initialize()
    {
        _skin = GetSkin();
        _title = GetTitle();
        _rect.center = new Vector2(Screen.width * 0.5f, Screen.height * 0.5f);
        _text = Enumerable.Range(0, 255).Select(idx => ((char)idx).ToString()).Aggregate((s1, s2) => s1 + s2);
        _text += _text += _text; // increase length a bit

        Console.WriteLine(_title + " font: " + _skin.font.name + ": " + string.Join(",", _skin.font.fontNames));
    }

    protected abstract GUISkin GetSkin();
    protected abstract string GetTitle();

    public void DrawWindow()
    {
        GUI.skin = _skin;
        _rect = KSPUtil.ClampRectToScreen(GUILayout.Window(_title.GetHashCode(), _rect, DoWindow, _title));
    }

    private void DoWindow(int winid)
    {
        GUILayout.TextArea(_text);
        GUI.DragWindow();
    }

}

class DynamicTextRenderer : TestTextRenderer
{
    private const string FontName = "Arial";
    private const int FontSize = 13;

    protected override GUISkin GetSkin()
    {
        var cloneSkin = UnityEngine.Object.Instantiate(HighLogic.Skin);

        var dynFont = Font.CreateDynamicFontFromOSFont(FontName, FontSize);

        cloneSkin.font = dynFont;

        return cloneSkin;
    }

    protected override string GetTitle()
    {
        return "Dynamic Text";
    }
}

class ResourceTextRenderer : TestTextRenderer
{
    private readonly Font[] _resourceFonts;
    private const string FontName = "Arial";

    public ResourceTextRenderer(Font[] resourceFonts)
    {
        if (resourceFonts == null) throw new ArgumentNullException("resourceFonts");
        _resourceFonts = resourceFonts;
    }

    protected override GUISkin GetSkin()
    {
        var cloneSkin = UnityEngine.Object.Instantiate(HighLogic.Skin);

        cloneSkin.font = _resourceFonts.First(f => f.fontNames.Contains(FontName));

        return cloneSkin;
    }

    protected override string GetTitle()
    {
        return "Resource-loaded Font";
    }
}

// controls ordering of text initialization and rendering
//  1. init resource font
//  2. init dyn font
//  3. make sure dyn font renders first
//  4. res font renders second
[KSPAddon(KSPAddon.Startup.Instantly, true)]
public class RenderOrderController : MonoBehaviour
{
    private DynamicTextRenderer _dynText = null;
    private ResourceTextRenderer _resText = null;

    private void Awake()
    {
        Font.textureRebuilt += FontOnTextureRebuilt;

        _resText = new ResourceTextRenderer(Resources.FindObjectsOfTypeAll<Font>());
        _resText.Initialize();

        _dynText = new DynamicTextRenderer();
        _dynText.Initialize();
    }

    private void OnDestroy()
    {
        Font.textureRebuilt -= FontOnTextureRebuilt;
    }

    private void FontOnTextureRebuilt(Font font)
    {
        Console.WriteLine("Font rebuilt: {0}; {1}", font.name, string.Join(",", font.fontNames));
    }

    // These following timestamps simulate the order in which the parts of the code happen.
    //
    // When they happen in a different order, that changes whether the bug happens:
    //
    // The timing is being controlled this way because if you just do all the steps
    // during the same OnGUI pass, then the order they happen within the body of the
    // one OnGUI call doesn't matter at all.  It only matters
    // when they happen in different passes (my kOS code is doing it in different
    // classes, not all in the same one like in this test example program):
    //
    // These don't have to be a whole two seconds apart from each other like this.  I only
    // did it that slowly to make it easier to watch it happen on the screen:

    // This is the ordering that triggers the bug.  (the Res font window is blank, but the Dyn font window is okay).
    private long checkAllFontsTime = 2000;
    private long startResFontTime = 4000;
    private long startDynFontTime = 6000;

    // Doing it in this order will avoid the bug:  (comment out the above lines and enable these instead):
    // private long startResFontTime = 2000;
    // private long checkAllFontsTime = 4000;
    // private long startDynFontTime = 6000;

    private System.Diagnostics.Stopwatch timer = null;

    private void OnGUI()
    {
        // On the first OnGui pass, start the timer:
        if (timer == null)
        {
            timer = new System.Diagnostics.Stopwatch();
            timer.Start();
        }

        var origSkin = GUI.skin;

        if (timer.ElapsedMilliseconds >= startResFontTime)
        {
            GUI.skin = origSkin;
            _resText.DrawWindow();
        }
        if (timer.ElapsedMilliseconds >= startDynFontTime)
        {
            GUI.skin = origSkin;
            _dynText.DrawWindow();
        }
        if (timer.ElapsedMilliseconds >= checkAllFontsTime)
        {
            CheckAllFonts();
            checkAllFontsTime = 999999999999; // Unlike the always-repainting text windows, this we only want to happen once.
        }

    }

    private static void CheckAllFonts()
    {
        int fontSize = 13; // font size to test the fonts at.

        // The bug only seems to happen when we iterate through all the fonts installed to build the
        // list, like this is simulating, and do so before the Arial font gets exercised.
        foreach (string fontName in Font.GetOSInstalledFontNames())
        {
            Font tempFont = Font.CreateDynamicFontFromOSFont(fontName, fontSize);
            bool result = IsFontMonospaced(tempFont);
            Console.WriteLine(string.Format("Font \"{0}\", monospaced={1}, therefore it {2} be included in the choices list.",
                fontName, result, (result ? "would" : "wouldn't")));
        }
    }

    // To xEvilReeperx:
    //   Here is a method copied verbatim from the kOS code that
    //   I used to perform the monospace check on each font.  I moved it into your
    //   example to see if it triggers the problem when exercised during
    //   the OnGUI call.
    // -----------------
    //
    /// <summary>A tool we can use to check if a font is monospaced by
    /// comparing the width of certain key letters.</summary>
    private static bool IsFontMonospaced(Font f)
    {
        CharacterInfo chInfo;
        int prevWidth;

        // Unity Lazy-loads the character info for the font.  Until you try
        // to actually render a character, it's CharacterInfo isn't populated
        // yet (all fields for the character's dimensions return a bogus zero value).
        // This next call forces Unity to load the information for the given characters
        // even though they haven't been rendered yet:
        f.RequestCharactersInTexture("XiW _i:");

        f.GetCharacterInfo('X', out chInfo);
        prevWidth = chInfo.advance;
        System.Console.WriteLine("eraseme: X advance is " + prevWidth);

        f.GetCharacterInfo('i', out chInfo);
        if (prevWidth != chInfo.advance)
            return false;
        prevWidth = chInfo.advance;
        System.Console.WriteLine("eraseme: i advance is " + prevWidth);

        f.GetCharacterInfo('W', out chInfo);
        if (prevWidth != chInfo.advance)
            return false;
        prevWidth = chInfo.advance;
        System.Console.WriteLine("eraseme: W advance is " + prevWidth);

        f.GetCharacterInfo(' ', out chInfo);
        if (prevWidth != chInfo.advance)
            return false;
        prevWidth = chInfo.advance;
        System.Console.WriteLine("eraseme: ' ' advance is " + prevWidth);

        f.GetCharacterInfo('_', out chInfo);
        if (prevWidth != chInfo.advance)
            return false;
        prevWidth = chInfo.advance;
        System.Console.WriteLine("eraseme: _ advance is " + prevWidth);

        f.GetCharacterInfo(':', out chInfo);
        if (prevWidth != chInfo.advance)
            return false;
        prevWidth = chInfo.advance;
        System.Console.WriteLine("eraseme: : advance is " + prevWidth);

        // That's probably a good enough test.  If all the above characters
        // have the same width, there's really good chance this is monospaced.

        return true;
    }

}

 

Edited by Steven Mading
Forgot to paste the code snip.
Link to comment
Share on other sites

Odd, your example works fine on my system. Do you end up triggering any texture rebuilds while running yours? Are you testing with the pre-release or the current version?

Win7

GTX 960

Current version (not localization pre-release)

Installed fonts according to Font.GetOSInstalledFontNames:

Spoiler

Aharoni Bold
Andalus
Angsana New
Angsana New Bold
Angsana New Italic
Angsana New Bold Italic
AngsanaUPC
AngsanaUPC Bold
AngsanaUPC Italic
AngsanaUPC Bold Italic
Aparajita
Aparajita Bold
Aparajita Italic
Aparajita Bold Italic
Arabic Typesetting
Arial
Arial Bold
Arial Italic
Arial Bold Italic
Arimo
Arimo Bold
Arimo Italic
Arimo Bold Italic
Batang
BatangChe
Browallia New
Browallia New Bold
Browallia New Italic
Browallia New Bold Italic
BrowalliaUPC
BrowalliaUPC Bold
BrowalliaUPC Italic
BrowalliaUPC Bold Italic
Buxton Sketch
Calibri
Calibri Bold
Calibri Italic
Calibri Bold Italic
Cambria
Cambria Bold
Cambria Italic
Cambria Bold Italic
Cambria Math
Candara
Candara Bold
Candara Italic
Candara Bold Italic
Comic Sans MS
Comic Sans MS Bold
Consolas
Consolas Bold
Consolas Italic
Consolas Bold Italic
Constantia
Constantia Bold
Constantia Italic
Constantia Bold Italic
Corbel
Corbel Bold
Corbel Italic
Corbel Bold Italic
Cordia New
Cordia New Bold
Cordia New Italic
Cordia New Bold Italic
CordiaUPC
CordiaUPC Bold
CordiaUPC Italic
CordiaUPC Bold Italic
Courier New
Courier New Bold
Courier New Italic
Courier New Bold Italic
Cuprum
Cuprum Bold
Cuprum Italic
Cuprum Bold Italic
DFKai-SB
DaunPenh
David
David Bold
DejaVu Sans
DejaVu Sans Bold
DejaVu Sans Italic
DejaVu Sans Bold Italic
DejaVu Sans Mono
DejaVu Sans Mono Bold
DejaVu Sans Mono Italic
DejaVu Sans Mono Bold Italic
DejaVu Serif
DejaVu Serif Bold
DejaVu Serif Italic
DejaVu Serif Bold Italic
DengXian
DilleniaUPC
DilleniaUPC Bold
DilleniaUPC Italic
DilleniaUPC Bold Italic
DokChampa
Dotum
DotumChe
Ebrima
Ebrima Bold
Estrangelo Edessa
EucrosiaUPC
EucrosiaUPC Bold
EucrosiaUPC Italic
EucrosiaUPC Bold Italic
Euphemia
FangSong
FrankRuehl
Franklin Gothic Medium
Franklin Gothic Medium Italic
FreesiaUPC
FreesiaUPC Bold
FreesiaUPC Italic
FreesiaUPC Bold Italic
Gabriola
Gautami
Gautami Bold
Gentium Basic
Gentium Basic Bold
Gentium Basic Italic
Gentium Basic Bold Italic
Gentium Book Basic
Gentium Book Basic Bold
Gentium Book Basic Italic
Gentium Book Basic Bold Italic
Georgia
Georgia Bold
Georgia Italic
Georgia Bold Italic
Gisha
Gisha Bold
Gulim
GulimChe
Gungsuh
GungsuhChe
Impact
IrisUPC
IrisUPC Bold
IrisUPC Italic
IrisUPC Bold Italic
Iskoola Pota
Iskoola Pota Bold
JasmineUPC
JasmineUPC Bold
JasmineUPC Italic
JasmineUPC Bold Italic
KaiTi
Kalinga
Kalinga Bold
Kartika
Kartika Bold
Khmer UI
Khmer UI Bold
KodchiangUPC
KodchiangUPC Bold
KodchiangUPC Italic
KodchiangUPC Bold Italic
Kokila
Kokila Bold
Kokila Italic
Kokila Bold Italic
Lao UI
Lao UI Bold
Latha
Latha Bold
Leelawadee
Leelawadee Bold
Levenim MT
Levenim MT Bold
LilyUPC
LilyUPC Bold
LilyUPC Italic
LilyUPC Bold Italic
Lucida Console
Lucida Sans Unicode
MS Gothic
MS Mincho
MS PGothic
MS PMincho
MS UI Gothic
MV Boli
Malgun Gothic
Malgun Gothic Bold
Mangal
Mangal Bold
Marlett
Meiryo
Meiryo Bold
Meiryo Italic
Meiryo Bold Italic
Meiryo UI
Meiryo UI Bold
Meiryo UI Italic
Meiryo UI Bold Italic
Micra
MicraC
MicraC Bold
Microsoft Himalaya
Microsoft JhengHei
Microsoft JhengHei Bold
Microsoft MHei
Microsoft MHei Bold
Microsoft NeoGothic
Microsoft NeoGothic Bold
Microsoft New Tai Lue
Microsoft New Tai Lue Bold
Microsoft PhagsPa
Microsoft PhagsPa Bold
Microsoft Sans Serif
Microsoft Tai Le
Microsoft Tai Le Bold
Microsoft Uighur
Microsoft YaHei
Microsoft YaHei Bold
Microsoft Yi Baiti
MingLiU
MingLiU-ExtB
MingLiU_HKSCS
MingLiU_HKSCS-ExtB
Miriam
Miriam Fixed
Mongolian Baiti
MoolBoran
NSimSun
Narkisim
Nyala
OpenSymbol
PMingLiU
PMingLiU-ExtB
Palatino Linotype
Palatino Linotype Bold
Palatino Linotype Italic
Palatino Linotype Bold Italic
Plantagenet Cherokee
Raavi
Raavi Bold
Rod
Sakkal Majalla
Sakkal Majalla Bold
Segoe Marker
Segoe Print
Segoe Print Bold
Segoe Script
Segoe Script Bold
Segoe UI
Segoe UI Bold
Segoe UI Italic
Segoe UI Bold Italic
Segoe UI Symbol
Segoe WP
Segoe WP Bold
Shonar Bangla
Shonar Bangla Bold
Shruti
Shruti Bold
SimHei
SimSun
SimSun-ExtB
Simplified Arabic
Simplified Arabic Bold
Simplified Arabic Fixed
SketchFlow Print
Sylfaen
Symbol
Tahoma
Tahoma Bold
Tele-Marines
Times New Roman
Times New Roman Bold
Times New Roman Italic
Times New Roman Bold Italic
Traditional Arabic
Traditional Arabic Bold
Trebuchet MS
Trebuchet MS Bold
Trebuchet MS Italic
Trebuchet MS Bold Italic
Tunga
Tunga Bold
Utsaah
Utsaah Bold
Utsaah Italic
Utsaah Bold Italic
Vani
Vani Bold
Verdana
Verdana Bold
Verdana Italic
Verdana Bold Italic
Vijaya
Vijaya Bold
Vrinda
Vrinda Bold
Webdings
Wingdings
Wingdings 3
WoWsSymbol
XVMSymbol
Yu Gothic
Yu Gothic Bold
dynamic

 

 

Edited by xEvilReeperx
Link to comment
Share on other sites

11 hours ago, xEvilReeperx said:

Odd, your example works fine on my system. Do you end up triggering any texture rebuilds while running yours? Are you testing with the pre-release or the current version?

Win7

GTX 960

Win7, Nvidia GTX 970, current release of KSP (not pre-release build).  I didn't check for textture rebuilds but I'll look at it tonight when I try again.

Perhaps the difference in our results has to do with exactly which fonts are installed on the system.  Maybe one particular font is breaking it for me when I iterate over all the fonts.  I know that when I don't iterate over all the fonts and just do the Arial font onlyl, I don't trigger the bug.  I could try culling the list down a few fonts at a time until it starts working to see if I can narrow down which causes the fail.  It's also possible that it's just the sheer number of fonts and that causing texture rebuilds differently.

Edited by Steven Mading
Link to comment
Share on other sites

I think I have a solution to my problem (that may also render moot the need for a shared font manager mod as described above, if my fix is reliable).

I tried forcibly calling DestroyImmediate(tempFont) after each font I draw in the foreach loop, like so:

        foreach (string fontName in Font.GetOSInstalledFontNames())
        {
            Font tempFont = Font.CreateDynamicFontFromOSFont(fontName, fontSize);
            bool result = IsFontMonospaced(tempFont);
            Console.WriteLine(string.Format("Font \"{0}\", monospaced={1}, therefore it {2} be included in the choices list.",
                fontName, result, (result ? "would" : "wouldn't")));
            DestroyImmediate(tempFont); // <----- This is the line I added -------
        }

 

I have no clue *why* that makes the bug go away, though.  I assumed that waiting for the garbage collector to clear out the orphaned font's data "when it gets around to it" would be good, as that's what you're usually supposed to do in a language like C#.  But by demanding that it force the font's data to go away right now before it starts testing the next font, the problem went away.

I wonder if there might be some kind of maximum number of fonts you can have active at a time (or maybe maximum number of glyphs you can have populated at once because it tries to render all of them onto one common texture of fixed size??).  If so that could explain why adding that line makes it go away.  Instead of the typical garbage collection pattern of "at the end of the loop you temporarily had 200 fonts loaded, but 191 of them were orphaned so I cleared them out and now you have only 9 fonts loaded again" it would now be "you have 9 fonts loaded at first, then I added one and now it's 10 fonts, then I deleted it and its 9 fonts again, then you added one and it was 10, then 9, then 10, then 9, ....)

If there was some kind of upper maximum, explicitly destroying the font every iteration would prevent it from reaching it.

That's the only guess I have as to a possible cause.

 

One distressing thing is that since you mentioned that the problem didn't surface on your computer, but it did on mine... that means I cannot trust that I really fixed it when I don't see it happening anymore.  It might still happen on other people's computers.  I'm going to have to ask our user community to help test this so we have people trying it on different graphics cards, different OS's, some using OpenGL some using DirectX, etc.

By the way, @xEvilReeperx - thanks again for the help on this so far.  Even reporting "this didn't happen for me" is extremely useful information to me.  (For example, it made me start thinking of the problem being just the sheer number of fonts perhaps - that plus the fact that I couldn't reproduce it when I just hand picked a few fonts to try, in the hope that I'd hit upon the "one problem font".)

 

Edited by Steven Mading
Link to comment
Share on other sites

On 28/03/2017 at 3:42 PM, Steven Mading said:

Then again, with SQUAD doing localizations in the next release, who knows if maybe even THEY might wait to load a font later on after game initialization

Squad uses TextMeshPro and that does not makes any uses of Unity native font system.

 

On 30/03/2017 at 8:31 AM, Steven Mading said:

I assumed that waiting for the garbage collector to clear out the orphaned font's data "when it gets around to it" would be good, as that's what you're usually supposed to do in a language like C#.  But by demanding that it force the font's data to go away right now before it starts testing the next font, the problem went away.

Destroy(Immediate) have nothing to do with the GC. It s a Unity call to remove a native Unity (C++) object.

 

My advice : Use TMP for your text :wink:. You can even make your own font (at least I can for you since I own the package in the proper version)

Link to comment
Share on other sites

12 hours ago, sarbian said:

Squad uses TextMeshPro and that does not makes any uses of Unity native font system.

 

Destroy(Immediate) have nothing to do with the GC. It s a Unity call to remove a native Unity (C++) object.

 

My advice : Use TMP for your text :wink:. You can even make your own font (at least I can for you since I own the package in the proper version)

How certain are you that it has nothing to do with GC?   The many code examples in Unity's docs show lots of cases of C# code just letting instances of Unity objects orphan away with no explicit Destroy() happening.  Therefore the Unity C# wrapper *must* be using something like hooks triggering on C#'s Dispose() calls to clean up its own C++ data that was created by instantiating the C# objects.  Otherwise everyone making C# plugins would be leaking memory all the time on a massive scale if they followed the examples provided by Unity themselves.  I think the Font's Destroy() work *has* to happen when a C# Dispose() happens, for this reason.  (Or some kind of reflections equivalent to using Dispose() because Unity is full of reflection usage). 

Although, that is another potential cause of the bug, come to think of it - what if Unity *usually* Destroy()s things when their C# wrapper gets orphaned, but made an exception for the Font class under the assumption that the same font will get used more than once and it's unlikely anyone would try to use lots of fonts once and then never use them ever again, which is the usage pattern I'm using.

At any rate, Unity was either not cleaning up the C++ data the Font class uses at all when it orphans, or it was but it was doing it too late.  Because when I told it to explicitly clean it up after each Font test instead of doing whatever pattern it normally follows by default, the bug disappeared.

And I've put out some tests for others in the kOS community to try and it's been a mix.  Most didn't see the bug happen, but a few did so I know it's not just my imagination.  (And for the few who did, trying my newer version where it explicitly does a Destroy instead of waiting for whatever would normally trigger it did fix it for them too).

 

 

As for TMP - is it compatible with IMGUI?  I was under the impression it was just for the new gui.  Thanks for the offer to make a font, but I'm not sure if that fits the need I was thinking of.  The need I was thinking of is this:  I don't speak Russian.  I am not qualified to decide if a font looks good in Cyrillic or not.  I don't speak Japanese.  I'm not qualified to decide if a font looks good in Kanji or not.  etc etc etc.  BUT, the person using kOS on their own computer, who does speak Russian, or speak Japanese, will be in a much better position than me to decide for themselves which font they think looks best for a terminal in their language, and they'll have such fonts at their own disposal on their own computer.

Unlike a normal video game maker, I'm not concerned with the "look and feel branding" of forcing a font choice on the user.  If I can make it easy for them to pick their own, it keeps everyone happy - or so I'm assuming.  Thus any solution that revolves around "ship with a hardcoded font" isn't really what I'm looking for.

 

 

Edited by Steven Mading
Link to comment
Share on other sites

5 hours ago, Steven Mading said:

How certain are you that it has nothing to do with GC?

I am. Anythings that inherits Unity.Object does not have its native part cleaned up auto magically. GameObject do on scene change but that s the only one AFAIK.

Link to comment
Share on other sites

8 hours ago, sarbian said:

I am. Anythings that inherits Unity.Object does not have its native part cleaned up auto magically. GameObject do on scene change but that s the only one AFAIK.

That really surprises me given how often the Unity C# documentation has you instantiating an object and then just letting it orphan.  Is all that stuff just sitting forever until a scene change?

 

 

Link to comment
Share on other sites

Okay thanks for the information.  That changes a lot about how things should be designed.  They shouldn't be done like normal C# software.  At all.  I'm surprised there isn't more memory leaking given how all the examples don't show you having to manually destroy things and instead just letting it happen whenever Unity feels like (which is apparently only on scene changes if what you're saying is true).

 

Edited by Steven Mading
Link to comment
Share on other sites

This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...