6
\$\begingroup\$

I was running the Visual Studio memory profiler on a project and noticed that an enormous amount of strings were being instantiated. It turns out that ContentManager.Load appears to create a new string instance each time it's called. This might not be a big deal on a PC, but on other platforms like the Xbox, it quickly forces a garbage collection.

Maybe I'm just a bit naive, but I assumed that ContentManager would manage my game content, given the name and all. This led to calling the Load method every time I needed a texture (like during Draw), but it seems like this approach apparently can't work since the Load method will allocate strings, even if it's not allocating extra memory for already-loaded textures.

Is there a good solution to this? The obvious answer seems to be to maintain my own references to the textures and not call Load if they've already been loaded...but that seems like precisely what the ContentManager should be doing.

\$\endgroup\$
1
  • 3
    \$\begingroup\$ You shouldn't be loading things on every draw call. Of course that's going to slow things down even if ContentManager caches them (which it does), because as you said, it allocates strings and has to look up the resources in the cache. You should load everything in the Load method that gets called once when your game screen/level/whatever is initialized and assigns the newly loaded objects to member variables. You would then reference those variables in your Draw call. \$\endgroup\$ Commented Apr 20, 2012 at 15:18

2 Answers 2

9
\$\begingroup\$

Your solution to the problem is correct - hold your own references.

In my games I take it a step further with class Asset<T>. Basically:

  • The Asset<T> type can exist in a not-yet-loaded state (a public field).
    • Asset<T> has some helper methods that make skipping unloaded assets easier.
  • Asset<T> objects are allocated by a class that implements AssetManager<T> (one implementation for each content type I care about). AssetManager<T> doesn't care about whether or not it's re-allocating duplicates. It is thread-safe.
  • AssetManager keeps track of already allocated objects and delegates calls to the various AssetManager<T> implementations. It is thread-safe. It is a static class (yeah, yeah, I know it shouldn't be).
  • When the GraphicsDevice is ready I spin up a secondary thread that goes and loads content from Asset<T> objects that are not yet loaded (basically just pop off a queue and load).
  • The AssetManager has a DoneLoading property that checks if the load queue is empty.
  • One implementation even had asset groups. This was used to e.g. keep universal assets in memory between levels.

The raw interface of these types would be:

public class Asset : IDisposable
{
  internal void Load(ContentManager mgr); // Call loader and assign it to Asset then set IsLoaded to true.
}

public class Asset<T> : Asset
{
  public string Name { get; } // Optional, I took it out for release builds.
  public T Asset { get; } // Throws exception if not loaded.
  public bool IsLoaded { get; }

  public Asset(Func<ContentManager, T> loader); // Store loader in a private field.
  public bool Use(Action<T> usage)
  {
    if (IsLoaded) { usage(Asset); return true; }
    return false;
  }
  /* E.g.
    foo.Use(tex =>
    {
      spriteBatch.Draw(tex, ...);
    });
  */
}

public class AssetManager<T>
{
  public Asset<T> Allocate(string name);
}

public static class AssetManager
{
  public static bool DoneLoading { get { return _notYetLoaded.Count == 0; } }
  private static Queue<Asset> _notYetLoaded;
  public static Asset<T> Get<T>(string name);
}

Advantages of this system:

  • You can ask for content before the GraphicsDevice is ready and they will be loaded when it is.
  • Load screens 'know' when to progress onto the game by simply checking the DoneLoading property on the AssetManager.
  • ContentManager is only asked for resources once - you won't get the string allocation problem.
  • You can animate your load screens.
  • By using Asset<T>.Use correctly assets would automagically appear in your game at runtime (e.g. if they are loaded inside the actual game state).

With this system I saw no significant problems with grabbing assets when graphical objects were constructed (and storing them in fields), but grabbing them each frame is just wasteful and lazy.

Also be careful about your ordering when it comes to adding and loading assets. You should always add the assets to the dictionary before they are loaded - this fixes cyclic dependency problems.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ You asked for a rope to hang yourself with and I gave you a bazooka to shoot yourself with ;). \$\endgroup\$ Commented Apr 20, 2012 at 13:13
  • \$\begingroup\$ Out of curiosity, is calling .Use with an anonymous method every Draw less efficient than, say, creating an OnDraw event and subscribing to it once on Load? \$\endgroup\$ Commented Apr 21, 2012 at 16:43
  • \$\begingroup\$ @RichardMarskell-Drackir you have the wrong idea with the event, but yes Use is marginally less efficient. Another way would be to write a implicit conversion (public static implicit operator T (Asset<T> asset)) that either throws an exception or returns the asset and manually use and if (asset.IsLoaded) everywhere. \$\endgroup\$ Commented Apr 22, 2012 at 9:33
4
\$\begingroup\$

ContentManager is for loading content, not keeping a reference to it*. Your "solution" as you call it is actually the way it should be done.

A good rule of thumb is only call ContentManager.Load<T>() when you need to load the content into memory. Once it is there keep your own reference to it.

Oh, and don't forget to call ContentManager.Unload() when you are done with everything in that scene.

Yes, internally it will do that, that is more for shared resources than anything else

\$\endgroup\$
1
  • \$\begingroup\$ Downvoter care to explain? \$\endgroup\$
    – user159
    Commented Apr 21, 2012 at 21:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .