/**
* Copyright (c) 2008-2012 Ardor Labs, Inc.
*
* This file is part of Ardor3D.
*
* Ardor3D is free software: you can redistribute it and/or modify it
* under the terms of its license which may be found in the accompanying
* LICENSE file or at <http://www.ardor3d.com/LICENSE>.
*/
package com.ardor3d.util;
import java.lang.ref.ReferenceQueue;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.logging.Logger;
import com.ardor3d.annotation.MainThread;
import com.ardor3d.image.Image;
import com.ardor3d.image.Texture;
import com.ardor3d.image.Texture2D;
import com.ardor3d.image.Texture3D;
import com.ardor3d.image.TextureCubeMap;
import com.ardor3d.image.TextureStoreFormat;
import com.ardor3d.image.util.ImageLoaderUtil;
import com.ardor3d.image.util.ImageUtils;
import com.ardor3d.renderer.ContextCleanListener;
import com.ardor3d.renderer.ContextManager;
import com.ardor3d.renderer.RenderContext;
import com.ardor3d.renderer.Renderer;
import com.ardor3d.renderer.RendererCallable;
import com.ardor3d.renderer.state.TextureState;
import com.ardor3d.util.resource.ResourceLocatorTool;
import com.ardor3d.util.resource.ResourceSource;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Multimap;
/**
* <code>TextureManager</code> provides static methods for building or retrieving a <code>Texture</code> object from
* cache.
*/
final public class TextureManager {
private static final Logger logger = Logger.getLogger(TextureManager.class.getName());
private static Map<TextureKey, Texture> _tCache = new MapMaker().weakKeys().weakValues().makeMap();
private static ReferenceQueue<TextureKey> _textureRefQueue = new ReferenceQueue<TextureKey>();
static {
ContextManager.addContextCleanListener(new ContextCleanListener() {
public void cleanForContext(final RenderContext renderContext) {
TextureManager.cleanAllTextures(null, renderContext, null);
}
});
}
private TextureManager() {}
/**
* Loads a texture by attempting to locate the given name using ResourceLocatorTool.
*
* @param name
* the name of the texture image.
* @param minFilter
* the filter for the near values. Used to determine if we should generate mipmaps.
* @param flipVertically
* If true, the image is flipped vertically during image loading.
* @return the loaded texture.
*/
public static Texture load(final String name, final Texture.MinificationFilter minFilter,
final boolean flipVertically) {
return load(ResourceLocatorTool.locateResource(ResourceLocatorTool.TYPE_TEXTURE, name), minFilter,
TextureStoreFormat.GuessNoCompressedFormat, flipVertically);
}
/**
* Loads a texture by attempting to locate the given name using ResourceLocatorTool.
*
* @param name
* the name of the texture image.
* @param minFilter
* the filter for the near values. Used to determine if we should generate mipmaps.
* @param format
* the specific format to use when storing this texture on the card.
* @param flipVertically
* If true, the image is flipped vertically during image loading.
* @return the loaded texture.
*/
public static Texture load(final String name, final Texture.MinificationFilter minFilter,
final TextureStoreFormat format, final boolean flipVertically) {
return load(ResourceLocatorTool.locateResource(ResourceLocatorTool.TYPE_TEXTURE, name), minFilter, format,
flipVertically);
}
/**
* Loads a texture from the given source.
*
* @param source
* the source of the texture image.
* @param minFilter
* the filter for the near values. Used to determine if we should generate mipmaps.
* @param flipVertically
* If true, the image is flipped vertically during image loading.
* @return the loaded texture.
*/
public static Texture load(final ResourceSource source, final Texture.MinificationFilter minFilter,
final boolean flipVertically) {
return load(source, minFilter, TextureStoreFormat.GuessNoCompressedFormat, flipVertically);
}
/**
* Loads a texture from the given source.
*
* @param source
* the source of the texture image.
* @param minFilter
* the filter for the near values. Used to determine if we should generate mipmaps.
* @param format
* the specific format to use when storing this texture on the card.
* @param flipVertically
* If true, the image is flipped vertically during image loading.
* @return the loaded texture.
*/
public static Texture load(final ResourceSource source, final Texture.MinificationFilter minFilter,
final TextureStoreFormat format, final boolean flipVertically) {
if (null == source) {
logger.warning("Could not load image... source was null. defaultTexture used.");
return TextureState.getDefaultTexture();
}
final TextureKey tkey = TextureKey.getKey(source, flipVertically, format, minFilter);
return loadFromKey(tkey, null, null);
}
/**
* Creates a texture from a given Ardor3D Image object.
*
* @param image
* the Ardor3D image.
* @param minFilter
* the filter for the near values. Used to determine if we should generate mipmaps.
* @return the loaded texture.
*/
public static Texture loadFromImage(final Image image, final Texture.MinificationFilter minFilter) {
return loadFromImage(image, minFilter, TextureStoreFormat.GuessNoCompressedFormat);
}
/**
* Creates a texture from a given Ardor3D Image object.
*
* @param image
* the Ardor3D image.
* @param minFilter
* the filter for the near values. Used to determine if we should generate mipmaps.
* @param format
* the specific format to use when storing this texture on the card.
* @return the loaded texture.
*/
public static Texture loadFromImage(final Image image, final Texture.MinificationFilter minFilter,
final TextureStoreFormat format) {
final TextureKey key = TextureKey.getKey(null, false, format, "img_" + image.hashCode(), minFilter);
return loadFromKey(key, image, null);
}
/**
* Load a texture from the given TextureKey. If imageData is given, use that, otherwise load it using the key's
* source information. If store is given, populate and return that Texture object.
*
* @param tkey
* our texture key. Must not be null.
* @param imageData
* optional Image data. If present, this is used instead of loading from source.
* @param store
* if not null, this Texture object is populated and returned instead of a new Texture object.
* @return the resulting texture.
*/
public static Texture loadFromKey(final TextureKey tkey, final Image imageData, final Texture store) {
if (tkey == null) {
logger.warning("TextureKey is null, cannot load");
return TextureState.getDefaultTexture();
}
Texture result = store;
// First look for the texture using the supplied key
final Texture cache = findCachedTexture(tkey);
if (cache != null) {
// look into cache.
if (result == null) {
result = cache.createSimpleClone();
if (result.getTextureKey() == null) {
result.setTextureKey(tkey);
}
return result;
}
cache.createSimpleClone(result);
return result;
}
Image img = imageData;
if (img == null) {
img = ImageLoaderUtil.loadImage(tkey.getSource(), tkey.isFlipped());
}
if (null == img) {
logger.warning("(image null) Could not load: " + tkey.getSource());
return TextureState.getDefaultTexture();
}
// Default to Texture2D
if (result == null) {
if (img.getDataSize() == 6) {
result = new TextureCubeMap();
} else if (img.getDataSize() > 1) {
result = new Texture3D();
} else {
result = new Texture2D();
}
}
result.setTextureKey(tkey);
result.setImage(img);
result.setMinificationFilter(tkey.getMinificationFilter());
result.setTextureStoreFormat(ImageUtils.getTextureStoreFormat(tkey.getFormat(), result.getImage()));
// Cache the no-context version
addToCache(result);
return result;
}
/**
* Add a given texture to the cache
*
* @param texture
* our texture
*/
public static void addToCache(final Texture texture) {
if (TextureState.getDefaultTexture() == null
|| (texture != TextureState.getDefaultTexture() && texture.getImage() != TextureState
.getDefaultTextureImage())) {
_tCache.put(texture.getTextureKey(), texture);
}
}
/**
* Locate a texture in the cache by key
*
* @param textureKey
* our key
* @return the texture, or null if not found.
*/
public static Texture findCachedTexture(final TextureKey textureKey) {
return _tCache.get(textureKey);
}
public static Texture removeFromCache(final TextureKey tk) {
return _tCache.remove(tk);
}
/**
* Delete all textures from card. This will gather all texture ids believed to be on the card and try to delete
* them. If a deleter is passed in, textures that are part of the currently active context (if one is active) will
* be deleted immediately. If a deleter is not passed in, we do not have an active context, or we encounter textures
* that are not part of the current context, then we will queue those textures to be deleted later using the
* GameTaskQueueManager.
*
* @param deleter
* if not null, this renderer will be used to immediately delete any textures in the currently active
* context. All other textures will be queued to delete in their own contexts.
*/
public static void cleanAllTextures(final Renderer deleter) {
cleanAllTextures(deleter, null);
}
/**
* Delete all textures from card. This will gather all texture ids believed to be on the card and try to delete
* them. If a deleter is passed in, textures that are part of the currently active context (if one is active) will
* be deleted immediately. If a deleter is not passed in, we do not have an active context, or we encounter textures
* that are not part of the current context, then we will queue those textures to be deleted later using the
* GameTaskQueueManager.
*
* If a non null map is passed into futureStore, it will be populated with Future objects for each queued context.
* These objects may be used to discover when the deletion tasks have all completed.
*
* @param deleter
* if not null, this renderer will be used to immediately delete any textures in the currently active
* context. All other textures will be queued to delete in their own contexts.
* @param futureStore
* if not null, this map will be populated with any Future task handles created during cleanup.
*/
public static void cleanAllTextures(final Renderer deleter, final Map<Object, Future<Void>> futureStore) {
// gather up expired textures... these don't exist in our cache
Multimap<Object, Integer> idMap = gatherGCdIds();
// Walk through the cached items and gather those too.
for (final TextureKey key : _tCache.keySet()) {
// possibly lazy init
if (idMap == null) {
idMap = ArrayListMultimap.create();
}
if (Constants.useMultipleContexts) {
final Set<Object> contextObjects = key.getContextObjects();
for (final Object o : contextObjects) {
// Add id to map
idMap.put(o, key.getTextureIdForContext(o));
}
} else {
idMap.put(ContextManager.getCurrentContext().getGlContextRep(), key.getTextureIdForContext(null));
}
key.removeFromIdCache();
}
// delete the ids
if (idMap != null && !idMap.isEmpty()) {
handleTextureDelete(deleter, idMap, futureStore);
}
}
/**
* Deletes all textures from card for a specific gl context. This will gather all texture ids believed to be on the
* card for the given context and try to delete them. If a deleter is passed in, textures that are part of the
* currently active context (if one is active) will be deleted immediately. If a deleter is not passed in, we do not
* have an active context, or we encounter textures that are not part of the current context, then we will queue
* those textures to be deleted later using the GameTaskQueueManager.
*
* If a non null map is passed into futureStore, it will be populated with Future objects for each queued context.
* These objects may be used to discover when the deletion tasks have all completed.
*
* @param deleter
* if not null, this renderer will be used to immediately delete any textures in the currently active
* context. All other textures will be queued to delete in their own contexts.
* @param context
* the context to delete for.
* @param futureStore
* if not null, this map will be populated with any Future task handles created during cleanup.
*/
public static void cleanAllTextures(final Renderer deleter, final RenderContext context,
final Map<Object, Future<Void>> futureStore) {
// gather up expired textures... these don't exist in our cache
Multimap<Object, Integer> idMap = gatherGCdIds();
final Object glRep = context.getGlContextRep();
// Walk through the cached items and gather those too.
for (final TextureKey key : _tCache.keySet()) {
// possibly lazy init
if (idMap == null) {
idMap = ArrayListMultimap.create();
}
final Integer id = key.getTextureIdForContext(glRep);
if (id != 0) {
idMap.put(context.getGlContextRep(), id);
key.removeFromIdCache(glRep);
}
}
// delete the ids
if (!idMap.isEmpty()) {
handleTextureDelete(deleter, idMap, futureStore);
}
}
/**
* Delete any textures from the card that have been recently garbage collected in Java. If a deleter is passed in,
* gc'd textures that are part of the currently active context (if one is active) will be deleted immediately. If a
* deleter is not passed in, we do not have an active context, or we encounter gc'd textures that are not part of
* the current context, then we will queue those textures to be deleted later using the GameTaskQueueManager.
*
* @param deleter
* if not null, this renderer will be used to immediately delete any gc'd textures in the currently
* active context. All other gc'd textures will be queued to delete in their own contexts.
*/
public static void cleanExpiredTextures(final Renderer deleter) {
cleanExpiredTextures(deleter, null);
}
/**
* Delete any textures from the card that have been recently garbage collected in Java. If a deleter is passed in,
* gc'd textures that are part of the currently active context (if one is active) will be deleted immediately. If a
* deleter is not passed in, we do not have an active context, or we encounter gc'd textures that are not part of
* the current context, then we will queue those textures to be deleted later using the GameTaskQueueManager.
*
* If a non null map is passed into futureStore, it will be populated with Future objects for each queued context.
* These objects may be used to discover when the deletion tasks have all completed.
*
* @param deleter
* if not null, this renderer will be used to immediately delete any gc'd textures in the currently
* active context. All other gc'd textures will be queued to delete in their own contexts.
* @param futureStore
* if not null, this map will be populated with any Future task handles created during cleanup.
*/
public static void cleanExpiredTextures(final Renderer deleter, final Map<Object, Future<Void>> futureStore) {
// gather up expired textures...
final Multimap<Object, Integer> idMap = gatherGCdIds();
// send to be deleted on next render.
if (idMap != null) {
handleTextureDelete(deleter, idMap, futureStore);
}
}
@SuppressWarnings("unchecked")
private static Multimap<Object, Integer> gatherGCdIds() {
Multimap<Object, Integer> idMap = null;
// Pull all expired textures from ref queue and add to an id multimap.
ContextIdReference<TextureKey> ref;
Integer id;
while ((ref = (ContextIdReference<TextureKey>) _textureRefQueue.poll()) != null) {
// lazy init
if (idMap == null) {
idMap = ArrayListMultimap.create();
}
if (Constants.useMultipleContexts) {
final Set<Object> contextObjects = ref.getContextObjects();
for (final Object o : contextObjects) {
id = ref.getValue(o);
if (id != null && id.intValue() != 0) {
// Add id to map
idMap.put(o, id);
}
}
} else {
id = ref.getValue(null);
if (id != null && id.intValue() != 0) {
idMap.put(ContextManager.getCurrentContext().getGlContextRep(), id);
}
}
ref.clear();
}
return idMap;
}
private static void handleTextureDelete(final Renderer deleter, final Multimap<Object, Integer> idMap,
final Map<Object, Future<Void>> futureStore) {
Object currentGLRef = null;
// Grab the current context, if any.
if (deleter != null && ContextManager.getCurrentContext() != null) {
currentGLRef = ContextManager.getCurrentContext().getGlContextRep();
}
// For each affected context...
for (final Object glref : idMap.keySet()) {
// If we have a deleter and the context is current, immediately delete
if (currentGLRef != null && (!Constants.useMultipleContexts || glref.equals(currentGLRef))) {
deleter.deleteTextureIds(idMap.get(glref));
}
// Otherwise, add a delete request to that context's render task queue.
else {
final Future<Void> future = GameTaskQueueManager.getManager(ContextManager.getContextForRef(glref))
.render(new RendererCallable<Void>() {
public Void call() throws Exception {
getRenderer().deleteTextureIds(idMap.get(glref));
return null;
}
});
if (futureStore != null) {
futureStore.put(glref, future);
}
}
}
}
@MainThread
public static void preloadCache(final Renderer r) {
for (final Texture t : _tCache.values()) {
if (t == null) {
continue;
}
if (t.getTextureKey().getSource() != null) {
r.loadTexture(t, 0);
}
}
}
static ReferenceQueue<TextureKey> getRefQueue() {
return _textureRefQueue;
}
}