// (c) Tuomas J. Lukka
package org.nongnu.libvob.gl.virtualtexture;
import org.nongnu.libvob.*;
import org.nongnu.libvob.gl.*;
import org.nongnu.libvob.util.Background;
import org.nongnu.libvob.util.ThreadBackground;
import java.awt.Dimension;
import java.io.*;
import java.util.*;
import java.util.zip.*;
/** An IndirectMipzipManager that doesn't delete or move
* textures in OpenGL (as a workaround for driver problems).
* shall not get glDeleteTexture()d or re-teximaged
* but instead, when detail levels are shifted,
* are shifted around. Only uses glTexSubImage() and glCompressedTexSubImage
* after initialization.
*
* There are two main issues that make this class complicated:
* 1) textures should be loaded
* from the disk (zip files) in a background thread, and
* 2) the OpenGL thread must be kept occupied only for short amounts
* of time at a time, to avoid loss of interactivity.
*/
public class NonDeletingIndirectMipzipManager {
public static boolean dbg = true;
private static void pa(String s) { System.out.println(s); }
// ---- Final metadata
private final String format;
private final boolean isCompressedFormat;
private final int width, height;
private final int bitsPerTexel;
private final int nlevels;
private final int[] levelWidths;
private final int[] levelHeights;
// ---- Stable temporary data arrays
/** A mipmap array stored in memory, with a given maximum detail
* level.
* LOCKING: all data is locked on a per-mipmap level,
* by locking levels[level].
*/
private class MipmapArray {
/** The minimum level that this object should
* contain data for.
*/
int minLevel;
/** The levels.
*/
byte[][] levels = new byte[nlevels][];
/** The number of bytes of data
* in a level.
* -1 = should be loaded, 0 = couldn't load,
* greater than 0 = contains the data.
*/
int[] levelSizes = new int[nlevels];
/** What the data in each level array is for.
*/
VirtualTexture[] virtualTextures = new VirtualTexture[nlevels];
MipmapArray(int minLevel) {
this.minLevel = minLevel;
for(int i=minLevel; i=0; i--) {
synchronized(levels[i]) {
if(virtualTextures[i] != null && levelSizes[i] == -1) {
// load this level
// This may change the member we locked,
// but it's ok at this point.
levels[i] =
virtualTextures[i].mipzipFile.getLevelData(i,
levels[i]);
// Mark it loaded by setting the size
levelSizes[i] =
virtualTextures[i].mipzipFile.getLevelSize(i);
return true;
}
}
}
return false;
}
public void setLoadRequest(int level, VirtualTexture virtualTexture) {
synchronized(levels[level]) {
levelSizes[level] = -1;
virtualTextures[level] = virtualTexture;
}
}
}
/** The termporary array where data can be left
* for a longer time.
* No element 0.
*/
private MipmapArray stableTmp;
/** The temporary array to use for swapping
* data between slots.
* No element 0.
*/
private MipmapArray swapTmp;
/** The temporary array to use for loading from disk.
*/
private MipmapArray loadTmp;
// ---- The loaded state
/** The state of a single texture slot.
* A single VirtualTexture can be bound to the texture at a time.
*/
private class Slot {
/** The base level of this slot.
*/
final int slotLevel;
/** The index of this slot within the level.
*/
final int slotIndex;
/** The texture.
* To make sure no OpenGL implementation will reserve
* too much space, the texture's level 0 is the level "slotLevel"
* of the real texture size.
*/
final GL.Texture texture = GL.createTexture();
/** The virtual image that the image in this slot is currently
* associated to.
*/
VirtualTexture currentImage;
/** Bitmap: which levels have been loaded.
* Index: absolute levels.
*/
final boolean[] levelsLoaded = new boolean[nlevels];
/** Base level (absolute) currently set.
*/
int baselevel;
Slot(int level, int index) {
this.slotLevel = level;
this.slotIndex = index;
for(int i=0; i < nlevels - slotLevel; i++) {
texture.loadNull2D("TEXTURE_2D", i,
format,
levelWidths[i + slotLevel],
levelHeights[i + slotLevel], 0,
"RGB", "FLOAT");
}
}
/** Change image assigned to slot.
* If assigning an image to another slot,
* it's important to *first* assign the image away
* from its current slot (using changeImage(null) if
* no other image is replacing it yet) and only then
* changeImage(im)
* on the new slot.
*/
synchronized void changeImage(VirtualTexture newImage) {
if(currentImage != null) {
currentImage.indirectTexture.setTexture(null);
virtualImage2slot.remove(currentImage);
}
for(int i=0; i=0; i--) {
if(!levelsLoaded[i]) {
// Allow the 1x1 mipmap image to be
// reused between textures - avoid
// having an inconsistent state and blacking
// out stuff.
baselevel = i + 1;
if(i == nlevels) i--;
texture.setTexParameter("TEXTURE_2D",
"TEXTURE_BASE_LEVEL", i - slotLevel +1);
return;
}
}
baselevel = slotLevel;
texture.setTexParameter("TEXTURE_2D",
"TEXTURE_BASE_LEVEL", slotLevel );
}
/** Save data of a level to a byte array.
* If there's not enough room in the data[][] array, makes more.
* @param level The absolute mipmap level to save
* @param data The data array
* @param index The index of data to use
* @return Data size saved.
*/
synchronized int saveData(int level, byte[][] data, int index) {
if(dbg) pa("SaveData "+this+" "+level+" "+data[index]);
int size = (int)(texture.getLevelParameter(level - slotLevel,
"TEXTURE_COMPRESSED_IMAGE_SIZE_ARB")[0]);
if(data[index].length < size)
data[index] = new byte[size];
texture.getCompressedTexImage(level - slotLevel, data[index]);
return size;
}
/** Load data from a byte array.
* @param level The **absolute** mipmap level
*/
synchronized void loadData(int level, byte[] data, int size) {
if(dbg) pa("LoadData: "+this+" "+level+" "+data+" "+size);
if(level < slotLevel)
throw new Error("Tried to load too high a level");
if(size <= 0)
throw new Error("LoadData: NO DATA???");
texture.compressedTexSubImage2D(level - slotLevel,
0, 0, levelWidths[level], levelHeights[level],
format, size, data);
levelsLoaded[level] = true;
}
/** Save all levels above the given absolute level
* to the temporary array.
*/
synchronized void saveToTmp(MipmapArray into, int minLevel) {
for(int i=minLevel; i= slotLevel &&
from.levels[i] != null &&
(!levelsLoaded[i]) &&
from.virtualTextures[i] == this.currentImage) {
synchronized(from.levels[i]) {
// Have to make sure this still stands
// in the synchronized section
if(from.virtualTextures[i] == this.currentImage) {
if(from.levelSizes[i] > 0)
loadData(i, from.levels[i], from.levelSizes[i]);
if(from.levelSizes[i] == 0) {
from.levelSizes[i] = -1;
from.virtualTextures[i] = null;
}
}
}
}
}
setBaseLevel();
}
public String toString() {
return "[Slot: "+slotLevel+" "+slotIndex+"]";
}
}
Slot[][] slots;
/** Mapping of virtual images to Slot objects.
* The Slot objects contain their indices in the slots array,
* which makes it easy to just store the Slot here.
*/
HashMap virtualImage2slot = new HashMap();
/** The images that should eventually be stored in each slot.
*/
VirtualTexture[][] slotTargets;
/** Mapping of virtual images to target Slot objects.
* The inverse of slotTargets.
*/
HashMap virtualImage2targetSlot = new HashMap();
boolean targetsChanged;
// ---- External API
/** Create a new NonDeletingIndirectMipzipManager.
* @param format The GL texture format string,
* e.g. COMPRESSED_RGB_S3TC_DXT1_EXT.
*/
public NonDeletingIndirectMipzipManager(String format, int width, int height) {
this.format = format.intern();
this.bitsPerTexel = GL.bitsPerTexel(format);
this.width = width;
this.height = height;
int w = width, h = height;
int nlevels = 0;
while(w > 0 && h > 0) {
nlevels ++;
w /= 2;
h /= 2;
}
this.nlevels = nlevels;
this.levelWidths = new int[nlevels];
this.levelHeights = new int[nlevels];
w = width; h = height;
for(int i=0; i