/*
* Aphelion
* Copyright (c) 2013 Joris van der Wel
*
* This file is part of Aphelion
*
* Aphelion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* Aphelion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Aphelion. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, the following supplemental terms apply, based on section 7 of
* the GNU Affero General Public License (version 3):
* a) Preservation of all legal notices and author attributions
* b) Prohibition of misrepresentation of the origin of this material, and
* modified versions are required to be marked in reasonable ways as
* different from the original version (for example by appending a copyright notice).
*
* Linking this library statically or dynamically with other modules is making a
* combined work based on this library. Thus, the terms and conditions of the
* GNU Affero General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your
* choice, provided that you also meet, for each linked independent module,
* the terms and conditions of the license of that module. An independent
* module is a module which is not derived from or based on this library.
*/
package aphelion.client.resource;
import aphelion.shared.resource.ResourceDB;
import aphelion.shared.event.LoopEvent;
import aphelion.shared.event.Workable;
import aphelion.shared.event.WorkerTask;
import aphelion.shared.event.promise.PromiseException;
import aphelion.shared.event.promise.PromiseRejected;
import aphelion.shared.event.promise.PromiseResolved;
import aphelion.shared.swissarmyknife.ThreadSafe;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.opengl.ImageDataFactory;
import org.newdawn.slick.opengl.InternalTextureLoader;
import org.newdawn.slick.opengl.LoadableImageData;
/**
* Loads (slick!) textures asynchronously.
* An attempt to load a texture will return an AsyncTexture object immediately.
* When attempting to draw a blank placeholder texture will be used if loading is not yet complete.
*
* This class lets you get a lot of references to a bunch of textures while the game is
* running without having to worry about large delays in frame rendering.
*
* This object must be added as a loop event to the loop doing the OpenGL rendering.
*
* Note that the final step (loading into texture memory) still has to be done on the render thread.
*
* Only use this object in the thread doing the opengl rendering.
*
* @author Joris
*/
public class AsyncTextureLoader implements LoopEvent
{
private static final Logger log = Logger.getLogger("aphelion.client.graphics");
// TODO: perhaps a better way of expiring items in the cache
private final HashMap<String, WeakReference<AsyncTexture>> textureCache = new HashMap<String, WeakReference<AsyncTexture>>();
final private ResourceDB db;
final private Workable workable;
private AtomicInteger pending = new AtomicInteger(0);
private ConcurrentLinkedQueue<Integer> releaseQueue = new ConcurrentLinkedQueue<Integer>();
private int cleanupCounter = 0;
/**
*
* @param db
* @param workable The main thread of this workable should be
* the thread that is doing the opengl rendering.
*/
public AsyncTextureLoader(ResourceDB db, Workable workable)
{
this.db = db;
this.workable = workable;
}
public ResourceDB getResourceDB()
{
return this.db;
}
@ThreadSafe
void finalizeTexture(AsyncTexture texture)
{
// Make sure not to reference texture outside of this method!
// this method is also used in a finalizer
if (texture.isLoaded() && !texture.isReleased())
{
texture.released = true;
releaseQueue.add(texture.textureID);
}
}
/** Are one or more textures currently being loaded?.
* This could be used to present a load screen while important
* textures are being loaded (such as ui elements, tileset, etc).
*
* @return
*/
@ThreadSafe
public boolean isLoadingSomething()
{
return pending.get() > 0;
}
/** Asynchronously load a texture.
* While the texture is not yet loaded, a placeholder texture will be displayed instead.
* Note that this means getWidth(), getTextureWidth() etc might return different values
* after the loading has completed.
* @param resourceKey A ResourceDB key. If the key does not exist, an AsyncTexture instance
* will still be returned (that will never complete loading). An error will be logged however.
* Also see db.resourceExists()
* @return
*/
@ThreadSafe
public AsyncTexture getTexture(String resourceKey)
{
AsyncTexture texture = null;
synchronized(textureCache)
{
WeakReference<AsyncTexture> cacheValue = textureCache.get(resourceKey);
if (cacheValue != null)
{
texture = cacheValue.get();
if (texture != null)
{
return texture;
}
}
if (texture == null)
{
texture = new AsyncTexture(this, resourceKey, GL11.GL_TEXTURE_2D);
texture.target = GL11.GL_TEXTURE_2D;
TextureCallback cb = new TextureCallback(texture);
workable.addWorkerTask(new TextureWorker(db), resourceKey).then((PromiseResolved) cb).then((PromiseRejected) cb);
pending.incrementAndGet();
textureCache.put(resourceKey, new WeakReference<>(texture));
}
if (++cleanupCounter > 10)
{
cleanupCounter = 0;
Iterator<WeakReference<AsyncTexture>> it = textureCache.values().iterator();
while (it.hasNext())
{
if (it.next().get() == null)
{
it.remove();
}
}
}
}
return texture;
}
@ThreadSafe
public void removeFromCache(AsyncTexture texture)
{
synchronized(textureCache)
{
WeakReference<AsyncTexture> cacheValue = textureCache.get(texture.resourceKey);
if (cacheValue != null && cacheValue.get() == texture)
{
textureCache.remove(texture.resourceKey);
}
}
}
@Override
public void loop(long systemNanoTime, long sourceNanoTime)
{
Integer textureID;
while ( (textureID = releaseQueue.poll()) != null)
{
ByteBuffer temp = ByteBuffer.allocateDirect(4);
temp.order(ByteOrder.nativeOrder());
IntBuffer texBuf = temp.asIntBuffer();
texBuf.put(textureID);
texBuf.flip();
GL11.glDeleteTextures(texBuf);
}
}
private static class TextureWorker extends WorkerTask<String, Object[]>
{
private ResourceDB db;
TextureWorker(ResourceDB db)
{
this.db = db;
}
@Override
public Object[] work(String resourceKey) throws PromiseException
{
// avoid the resource cache, AsyncTextureLoader has its own cache
InputStream in = db.getInputStreamSync(resourceKey);
ResourceDB.FileEntry fileEntry = db.getFileEntry(resourceKey);
if (in == null || fileEntry == null)
{
log.log(Level.SEVERE, "Unable to load texture, the given resource key ({0}) does not exist", resourceKey);
throw new InvalidResourceKeyException();
}
String fileName = fileEntry.zipEntry == null ? fileEntry.file.getName() : fileEntry.zipEntry;
LoadableImageData imageData = ImageDataFactory.getImageDataFor(fileName);
try
{
ByteBuffer textureBytes = imageData.loadImage(new BufferedInputStream(in), false, null);
if (textureBytes == null)
{
throw new IOException("loadImage returned null");
}
log.log(Level.INFO, "Texture data for {0} read. {1} {2} {3} {4} {5}", new Object[] {
resourceKey,
imageData.getWidth(),
imageData.getHeight(),
imageData.getDepth(),
imageData.getTexWidth(),
imageData.getTexHeight()
});
return new Object[] {
textureBytes,
imageData.getWidth(),
imageData.getHeight(),
imageData.getDepth(),
imageData.getTexWidth(),
imageData.getTexHeight()
};
}
catch (IOException | UnsatisfiedLinkError | UnsupportedOperationException ex)
{
log.log(Level.SEVERE, "Exception while parsing image for texture " + resourceKey, ex);
throw new PromiseException(ex);
}
}
}
private class TextureCallback implements PromiseResolved, PromiseRejected
{
private final AsyncTexture texture;
TextureCallback(AsyncTexture texture)
{
this.texture = texture;
}
@Override
public Object resolved(Object ret_) throws PromiseException
{
pending.decrementAndGet();
Object[] ret = (Object[]) ret_;
ByteBuffer textureBytes = (ByteBuffer) ret[0];
int imageWidth = (Integer) ret[1];
int imageHeight = (Integer) ret[2];
boolean hasAlpha = ((Integer) ret[3]) == 32;
int texWidth = (Integer) ret[4];
int texHeight = (Integer) ret[5];
int srcPixelFormat = hasAlpha ? GL11.GL_RGBA : GL11.GL_RGB;
int componentCount = hasAlpha ? 4 : 3;
IntBuffer temp = BufferUtils.createIntBuffer(16);
GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE, temp);
int max = temp.get(0);
if ((texWidth > max) || (texHeight > max))
{
texture.error = true;
log.log(Level.SEVERE, "Attempt to allocate a texture too big for the current hardware");
return null;
}
texture.textureID = InternalTextureLoader.createTextureID();
GL11.glBindTexture(texture.target, texture.textureID);
// todo: different filters?
GL11.glTexParameteri(texture.target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(texture.target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GL11.glTexImage2D(
texture.target,
0,
GL11.GL_RGBA8,
InternalTextureLoader.get2Fold(imageWidth),
InternalTextureLoader.get2Fold(imageHeight),
0,
srcPixelFormat,
GL11.GL_UNSIGNED_BYTE,
textureBytes);
texture.imageWidth = imageWidth;
texture.imageHeight = imageHeight;
texture.texWidth = texWidth;
texture.texHeight = texHeight;
texture.alpha = hasAlpha;
texture.widthRatio = (float)imageWidth / texWidth;
texture.heightRatio = (float)imageHeight / texHeight;
texture.error = false;
texture.loaded();
log.log(Level.INFO, "Texture {0} loaded. {1}", new Object[] { texture.getResourceKey(), hasAlpha});
return null;
}
@Override
public void rejected(PromiseException error)
{
pending.decrementAndGet();
texture.error = true;
}
}
@SuppressWarnings("serial")
public static class InvalidResourceKeyException extends PromiseException { }
}