/*
* JaamSim Discrete Event Simulation
* Copyright (C) 2012 Ausenco Engineering Canada Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jaamsim.render;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import com.jaamsim.ui.LogBox;
import com.jogamp.opengl.GL2GL3;
import com.jogamp.opengl.GLException;
import com.jogamp.opengl.GLExtensions;
/**
* A cache that ensures each texture object is only loaded once, looks up textures by URL to there
* is a chance of a repeated texture if synonymous URLs are used
* @author Matt.Chudleigh
*
*/
public class TexCache {
private static final int MAX_UNCOMPRESSED_SIZE = 64*1024*1024; // No texture can be more than 64 megs uncompressed
private static class TexEntry {
public int texID;
public boolean hasAlpha;
public boolean compressed;
public boolean forcedCompressed;
public TexEntry(int id, boolean alpha, boolean compressed, boolean forcedCompressed) {
this.texID = id;
this.hasAlpha = alpha;
this.compressed = compressed;
this.forcedCompressed = forcedCompressed;
}
}
private static class LoadingEntry {
public int bufferID;
public URI imageURI;
public boolean hasAlpha;
public boolean compressed;
public boolean forcedCompressed; // The user did not request a compressed texture, but we compressed it anyway
public ByteBuffer data;
public int width, height;
public AtomicBoolean done = new AtomicBoolean(false);
public AtomicBoolean failed = new AtomicBoolean(false);
public final Object lock = new Object();
public LoadingEntry(URI uri, ByteBuffer data, boolean alpha, boolean compressed, boolean forcedCompressed) {
this.imageURI = uri;
this.data = data;
this.hasAlpha = alpha;
this.compressed = compressed;
this.forcedCompressed = forcedCompressed;
}
}
private final Map<String, TexEntry> _texMap = new HashMap<>();
private final Map<String, LoadingEntry> _loadingMap = new HashMap<>();
private final EntryLoaderRunner entryLoader = new EntryLoaderRunner();
private final Renderer _renderer;
public static final URI BAD_TEXTURE;
private int badTextureID = -1;
public static final int LOADING_TEX_ID = -2;
static {
try {
BAD_TEXTURE = TexCache.class.getResource("/resources/images/bad-texture.png").toURI();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
public TexCache(Renderer r) {
_renderer = r;
}
public void init(GL2GL3 gl) {
LoadingEntry badLE = launchLoadImage(gl, BAD_TEXTURE, false, false);
assert(badLE != null); // We should never fail to load the bad texture
waitForTex(badLE);
_loadingMap.remove(BAD_TEXTURE.toString());
badTextureID = loadGLTexture(gl, badLE);
assert(badTextureID != -1); // Hopefully OpenGL never naturally returns -1, but I don't think it should
}
public int getTexID(GL2GL3 gl, URI imageURI, boolean withAlpha, boolean compressed, boolean waitUntilLoaded) {
if (imageURI == null) {
return badTextureID;
}
// Scan the list of textures and load any that are ready
ArrayList<String> loadedStrings = new ArrayList<>();
for (Map.Entry<String, LoadingEntry> entry : _loadingMap.entrySet()) {
LoadingEntry le = entry.getValue();
if (le.done.get()) {
loadedStrings.add(entry.getKey());
int glTexID = loadGLTexture(gl, le);
_texMap.put(le.imageURI.toString(), new TexEntry(glTexID, le.hasAlpha, le.compressed, le.forcedCompressed));
}
}
for (String s : loadedStrings) {
_loadingMap.remove(s);
}
String imageURIKey = imageURI.toString();
if (_texMap.containsKey(imageURIKey)) {
// There is an entry in the cache, but let's check the other attributes
TexEntry entry = _texMap.get(imageURIKey);
boolean found = true;
if (withAlpha && !entry.hasAlpha) {
found = false; // This entry does not have an alpha channel
}
if (entry.compressed && !compressed && !entry.forcedCompressed) {
// The entry is compressed, but we requested an uncompressed image
found = false;
}
if (found) {
return entry.texID;
}
// The entry exists, but not as was requested, free the texture so we can reload it
int[] texIDs = new int[1];
texIDs[0] = entry.texID;
gl.glDeleteTextures(1, texIDs, 0);
_texMap.remove(imageURIKey);
}
boolean isLoading = _loadingMap.containsKey(imageURIKey);
LoadingEntry le = null;
if (!isLoading) {
le = launchLoadImage(gl, imageURI, withAlpha, compressed);
if (le == null) {
// The image could not be found
_texMap.put(imageURIKey, new TexEntry(badTextureID, withAlpha, compressed, false));
return badTextureID;
}
}
if (!waitUntilLoaded && _renderer.allowDelayedTextures()) {
return LOADING_TEX_ID;
}
waitForTex(le);
_loadingMap.remove(imageURIKey);
int glTexID = loadGLTexture(gl, le);
_texMap.put(le.imageURI.toString(), new TexEntry(glTexID, le.hasAlpha, le.compressed, le.forcedCompressed));
return glTexID;
}
private LoadingEntry launchLoadImage(GL2GL3 gl, final URI imageURI, boolean transparent, boolean compressed) {
Dimension dim = getImageDimension(imageURI);
if (dim == null) {
// Could not load image
LogBox.formatRenderLog("Could not load image URL: %s\n", imageURI.toString());
return null;
}
// Map an openGL buffer of size width*height*4
int[] ids = new int[1];
gl.glGenBuffers(1, ids, 0);
int bufferSize = dim.width*dim.height*4;
boolean forcedCompressed = false;
if (!transparent && !compressed) {
if (dim.width * dim.height * 3 > MAX_UNCOMPRESSED_SIZE) {
// Always compress large textures and save the user from themselves
compressed = true;
forcedCompressed = true;
}
}
if (compressed) {
assert(gl.isExtensionAvailable(GLExtensions.EXT_texture_compression_s3tc));
// Round width and height up to nearest multiple of 4 (the s3tc block size)
int width = dim.width;
if ((width&3)!= 0) {
width = (width&~3)+4;
}
int height = dim.height;
if ((height&3)!= 0) {
height = (height&~3)+4;
}
bufferSize = width * height / 2;
}
ByteBuffer mappedBuffer = null;
gl.glBindBuffer(GL2GL3.GL_PIXEL_UNPACK_BUFFER, ids[0]);
gl.glBufferData(GL2GL3.GL_PIXEL_UNPACK_BUFFER, bufferSize, null, GL2GL3.GL_STREAM_READ);
try {
mappedBuffer = gl.glMapBuffer(GL2GL3.GL_PIXEL_UNPACK_BUFFER, GL2GL3.GL_WRITE_ONLY);
} catch (GLException ex) {
// A GL Exception here is most likely caused by an out of memory, this is recoverable and simply use the bad texture
LogBox.formatRenderLog("Out of GRAM for image URL: %s\n", imageURI.toString());
return null;
}
// Explicitly check for an error (we may not be using a DebugGL implementation, so the exception may not be thrown)
if (gl.glGetError() != GL2GL3.GL_NO_ERROR) {
LogBox.formatRenderLog("GL Error loading image URL: %s\n", imageURI.toString());
return null;
}
gl.glBindBuffer(GL2GL3.GL_PIXEL_UNPACK_BUFFER, 0);
final LoadingEntry le = new LoadingEntry(imageURI, mappedBuffer, transparent, compressed, forcedCompressed);
le.bufferID = ids[0];
_loadingMap.put(imageURI.toString(), le);
entryLoader.loadEntry(le);
return le;
}
private int loadGLTexture(GL2GL3 gl, LoadingEntry le) {
if (le.failed.get()) {
return badTextureID;
}
int[] i = new int[1];
gl.glGenTextures(1, i, 0);
int glTexID = i[0];
gl.glBindTexture(GL2GL3.GL_TEXTURE_2D, glTexID);
if (le.compressed)
gl.glTexParameteri(GL2GL3.GL_TEXTURE_2D, GL2GL3.GL_TEXTURE_MIN_FILTER, GL2GL3.GL_LINEAR );
else
gl.glTexParameteri(GL2GL3.GL_TEXTURE_2D, GL2GL3.GL_TEXTURE_MIN_FILTER, GL2GL3.GL_LINEAR_MIPMAP_LINEAR );
gl.glTexParameteri(GL2GL3.GL_TEXTURE_2D, GL2GL3.GL_TEXTURE_MAG_FILTER, GL2GL3.GL_LINEAR );
gl.glTexParameteri(GL2GL3.GL_TEXTURE_2D, GL2GL3.GL_TEXTURE_WRAP_S, GL2GL3.GL_REPEAT);
gl.glTexParameteri(GL2GL3.GL_TEXTURE_2D, GL2GL3.GL_TEXTURE_WRAP_T, GL2GL3.GL_REPEAT);
gl.glPixelStorei(GL2GL3.GL_UNPACK_ALIGNMENT, 1);
// Attempt to load to a proxy texture first, then see what happens
int internalFormat = 0;
if (le.hasAlpha && le.compressed) {
// We do not currently support compressed textures with alpha
assert(false);
return badTextureID;
} else if(le.hasAlpha && !le.compressed) {
internalFormat = GL2GL3.GL_RGBA;
} else if(!le.hasAlpha && le.compressed) {
internalFormat = GL2GL3.GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
} else if(!le.hasAlpha && !le.compressed) {
internalFormat = GL2GL3.GL_RGB;
}
gl.glBindBuffer(GL2GL3.GL_PIXEL_UNPACK_BUFFER, le.bufferID);
gl.glUnmapBuffer(GL2GL3.GL_PIXEL_UNPACK_BUFFER);
try {
if (le.compressed) {
gl.glCompressedTexImage2D(GL2GL3.GL_TEXTURE_2D, 0, internalFormat, le.width,
le.height, 0, le.data.capacity(), 0);
_renderer.usingVRAM(le.data.capacity());
} else {
gl.glTexImage2D(GL2GL3.GL_TEXTURE_2D, 0, internalFormat, le.width,
le.height, 0, GL2GL3.GL_BGRA, GL2GL3.GL_UNSIGNED_INT_8_8_8_8_REV, 0);
_renderer.usingVRAM(le.width*le.height*4);
}
// Note we do not let openGL generate compressed mipmaps because it stalls the render thread really badly
// in theory it could be generated in the worker thread, but not yet
if (!le.compressed)
gl.glGenerateMipmap(GL2GL3.GL_TEXTURE_2D);
} catch (GLException ex) {
// We do not have enough texture memory
LogBox.renderLog(String.format("Error loading texture: %s", le.imageURI.toString()));
LogBox.renderLog(String.format(" %s", ex.toString()));
return badTextureID;
}
gl.glBindTexture(GL2GL3.GL_TEXTURE_2D, 0);
gl.glBindBuffer(GL2GL3.GL_PIXEL_UNPACK_BUFFER, 0);
i[0] = le.bufferID;
gl.glDeleteBuffers(1, i, 0);
// Finally queue a redraw in case an asset avoided drawing this one
_renderer.queueRedraw();
return glTexID;
}
public static Dimension getImageDimension(URI imageURI) {
ImageInputStream inStream = null;
try {
inStream = ImageIO.createImageInputStream(imageURI.toURL().openStream());
Iterator<ImageReader> it = ImageIO.getImageReaders(inStream);
if (it.hasNext()) {
ImageReader reader = it.next();
reader.setInput(inStream);
Dimension ret = new Dimension(reader.getWidth(0), reader.getHeight(0));
reader.dispose();
inStream.close();
return ret;
}
} catch (IOException ex) {
}
return null;
}
private class EntryLoaderRunner implements Runnable {
final ArrayList<LoadingEntry> list = new ArrayList<>();
private Thread loadThread = null;
@Override
public void run() {
while (true) {
LoadingEntry le = null;
synchronized (this) {
if (list.isEmpty()) {
loadThread = null;
return;
}
le = list.remove(0);
}
loadImage(le);
}
}
void loadEntry(LoadingEntry le) {
synchronized (this) {
list.add(le);
if (loadThread == null) {
loadThread = new Thread(this, "TextureLoadThread");
loadThread.start();
}
}
}
}
private void loadImage(LoadingEntry le) {
BufferedImage img = null;
try {
img = ImageIO.read(le.imageURI.toURL());
}
catch(Exception e) {
le.failed.set(true);
le.done.set(true);
return;
}
if (img == null) {
le.failed.set(true);
le.done.set(true);
return;
}
int width = img.getWidth();
int height = img.getHeight();
le.width = width;
le.height = height;
AffineTransform flipper = new AffineTransform(1, 0,
0, -1,
0, height);
BufferedImage bgr = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = bgr.createGraphics();
if (!le.hasAlpha) {
g2.setColor(Color.WHITE);
} else {
g2.setColor(new Color(0, 0, 0, 0));
}
g2.fillRect(0, 0, width, height);
g2.drawImage(img, flipper, null);
g2.dispose();
DataBufferInt ints = (DataBufferInt)bgr.getData().getDataBuffer();
if (le.compressed) {
S3TexCompressor comp = new S3TexCompressor();
IntBuffer intBuffer = (IntBuffer.wrap(ints.getData()));
ByteBuffer compressed = comp.compress(intBuffer, le.width, le.height);
le.data.put(compressed);
} else {
le.data.asIntBuffer().put(ints.getData());
}
le.done.set(true);
synchronized(le.lock) {
le.lock.notify();
}
_renderer.queueRedraw();
}
private void waitForTex(LoadingEntry le) {
synchronized (le.lock) {
while (!le.done.get()) {
try {
le.lock.wait();
} catch (InterruptedException ex) {}
}
}
}
}