// (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