Since I am dynamically loading Level of Detail textures, I needed to control the number of Texture2D objects being used.
In order to support a texture cache, I initially create a specifc number of Texture2D objects for each level of detail. When a new texture is required, I queue the image for loading using a background process as described previously. If the texture is still required when the image has loaded, I find the next available texture which has not been set on the GraphicsDevice and create the texture from the image data.
In order to maximise the work done on the background thread, and minimise the amount of Texture2D objects set on the GraphicsDevice, I combine both surface and specular maps in a single Texture2D by using the alpha channel for specularity (see Figure 1 below). In a similar way, I can combine normal and cloud maps in another Texture2D. A third texture is used for night-maps (see Figure 2 below), with the alpha channel still available for future use.
If a texture is required in the render loop but not yet cached, I check the cache for a parent texture and scale it in the shader until the child texture is available. If a texture is no longer required by the time that a corresponding set of image data has been loaded, I periodically expire the data to conserve memory. In addition, I only start loading image data when a texture has been requested repeatedly for a configurable interval. This means that I won't be loading data un-necessarily during fast flybys.